Skip to content
Jennifer Programming Language

binary - bulk operations on bytes

Enable with use binary;. Namespaced under binary.; every function is called as binary.NAME(...). It is to bytes what strings is to string and lists is to list: concatenate, slice, search, and split byte buffers.

The library is named binary because bytes is a reserved type keyword (it appears after as, as in def b as bytes;) and so cannot be a library namespace.

All indices and lengths are byte offsets, 0-relative. binary.slice(b, 0, 2) returns the first two bytes; len(b) is the byte count. (Contrast strings, which counts Unicode code points.)

Every operation is non-mutating and value-semantic. The inputs are never aliased or written; each result is a freshly-allocated bytes (or list of bytes / int / bool). Mutating a result never touches the source.

jennifer
use io;
use convert;
use binary;

def a as bytes init convert.bytesFromString("Hello, ", "utf-8");
def b as bytes init convert.bytesFromString("World", "utf-8");
def c as bytes init binary.concat($a, $b);

io.printf("%s\n", convert.stringFromBytes($c, "utf-8"));         # "Hello, World"
io.printf("%d\n", binary.indexOf($c, $b));                       # 7
io.printf("%s\n", convert.stringFromBytes(binary.slice($c, 7, 12), "utf-8")); # "World"
io.printf("%t\n", binary.startsWith($c, $a));                    # true

Functions

CallReturnsNotes
binary.make(n)bytesAllocate n zero bytes - the fixed-size buffer allocator (bytes has no literal). n in [0, 256 MiB].
binary.make(n, fill)bytesAs above, every byte set to fill (a byte value in [0, 255]).
binary.concat(a, b)bytesJoin two byte sequences. See Building buffers for the loop caveat.
binary.join(parts)bytesConcatenate every bytes in a list of bytes into one, in O(n). The byte-data strings.join.
binary.join(parts, sep)bytesAs above, with sep (a bytes) placed between adjacent pieces.
binary.slice(b, start)bytesFrom start to the end of b.
binary.slice(b, start, end)bytesThe half-open range [start, end); exclusive end.
binary.indexOf(haystack, needle)intByte index of the first occurrence of needle; -1 if absent. An empty needle returns 0. Same shape as strings.indexOf.
binary.indexOf(haystack, needle, from)intAs above but searching from byte offset from (in [0, len]); returns the absolute index, -1 if none, from for an empty needle. from = idx + 1 each step makes find-all one O(n) pass.
binary.indexOf(haystack, needle, from, limit)intAs above but over the half-open window [from, limit) (from <= limit <= len): a match is reported only when it lies wholly inside it, and the scan never reads past limit. Equivalent to indexOf(slice(haystack, from, limit), needle) without the copy - so a bounded probe does not overscan the rest of the buffer when the needle is absent.
binary.contains(haystack, needle)boolWhether needle occurs in haystack (the boolean sibling of indexOf, like strings.contains).
binary.split(b, sep)list of bytesSplit on every occurrence of a non-empty sep; preserves empty segments.
binary.startsWith(b, prefix)boolTrue iff b begins with prefix.
binary.endsWith(b, suffix)boolTrue iff b ends with suffix.

binary.split is the natural way to cut a container at a delimiter in one pass - for example splitting a MIME multipart body on its boundary, or a length-unprefixed record stream on its separator:

jennifer
def parts as list of bytes init binary.split($body, $boundary);

Errors

  • binary.slice(b, -1, 3) - negative start.
  • binary.slice(b, 0, 99) - end past the byte length.
  • binary.slice(b, 4, 2) - end before start.
  • binary.split(b, sep) with an empty sep.
  • Non-bytes arguments where bytes are required, or non-int where int is required (binary.slice(b, "3")).
  • Arity errors (too many or too few arguments).

All are positioned, catchable runtime errors.

Why this library exists: throughput

The reason to reach for binary over a hand-written loop is speed. A .j program that concatenates, slices, searches, or splits a byte buffer one byte at a time pays the tree-walking interpreter's per-operation cost on every byte (an index read, a comparison, an append, a loop step). For kilobyte-sized data that is invisible; for a megabyte-scale buffer it dominates. Each binary function pushes the inner loop into Go and runs it once at native speed (binary.indexOf / binary.split are Go's bytes.Index / bytes.Split), so scanning a large buffer for a delimiter is one call, not a per-byte scan.

jennifer
# Slow: a per-byte scan pays the interpreter's cost on every byte.
def idx as int init -1;
def i as int init 0;
while ($i + len($needle) <= len($buf) and $idx < 0) {
    # ... byte-by-byte comparison ...
    $i = $i + 1;
}

# Fast: one Go-speed scan.
def idx as int init binary.indexOf($buf, $needle);

Building buffers

binary.concat copies both inputs into a fresh result, so it is O(len(a) + len(b)) per call. Using it to accumulate a growing buffer in a loop ($buf = binary.concat($buf, $chunk) each iteration) re-copies the whole buffer every time, which is O(n^2) overall. To assemble a buffer from a stream, read it in one call instead: net.readAll reads a whole response body to EOF as one bytes, and net.readN reads an exact length-prefixed frame - both grow a single Go slice rather than concatenating in a .j loop. Reserve binary.concat for joining a small, fixed number of pieces.

When you already hold the pieces - you built them in a loop rather than read them off a stream - collect them in a list of bytes and make one binary.join call. That is O(n) (a single allocation sized to the total), where the concat-in-a-loop it replaces is O(n^2). It is the exact byte-data analogue of building a string with list-append plus strings.join:

jennifer
# Slow: each concat re-copies the whole accumulator - O(n^2).
def out as bytes;
for (def chunk in $chunks) {
    $out = binary.concat($out, $chunk);
}

# Fast: append the pieces, join once - O(n).
def parts as list of bytes init [];
for (def chunk in $chunks) {
    $parts[] = $chunk;
}
def out as bytes init binary.join($parts);

TinyGo

binary is pure byte manipulation with no host dependencies, so it runs in full on both binaries - the default jennifer and the constrained jennifer-tiny.

See also: strings, net (readAll / readN), index.md.