hanki

bytes_builder

stdlib/bytes_builder.hk: the BytesBuilder mutable byte buffer.

BytesBuilder is a runtime-managed, non-sendable native resource (HANKI.md §4): a growable byte buffer behind reference semantics, the O(n) way to assemble a bytes value. bytes itself is immutable, and a codec that appended to it would pay O(n^2); a builder accumulates in place and hands back the finished buffer.

Every operation is an action (!) with an empty effect row. Hanki's effect rows name real-world capabilities (io, fs, net, …); an in-memory mutation is none, and the rows are therefore empty. The ! is the separate, broader pure/impure boundary: a mutation is not referentially transparent, and pure code may not perform it. new! is an action for the same reason: each call mints a fresh, independently mutable identity, which a pure constructor could not promise. The mutation never escapes into the value model: there is still no way to mutate a bytes, a struct field, or a list.

The primitive operations are @intrinsic; lowering emits Op::HostCall and the scheduler dispatches into the registered Rust closure (bytecode), or the AOT C-ABI shim mutates the boxed buffer in place.

BytesBuilder

BytesBuilder, or bytes.BytesBuilder, is a runtime-managed native resource handle (HANKI.md §4): live native state the per-actor memory manager owns, released when the last handle drops. It is single-owner - never copied, moved across a send - and has no fields of its own, so its methods are its whole surface. A handle is minted by an API that opens one; it is never constructed.

impl BytesBuilder

new!

def new!() -> BytesBuilder

A new, empty builder.

b = BytesBuilder.new!()
b.finish!() => "".to_bytes()

push!

def push!(self, b: u8) -> ()

Append one byte to the end of the buffer.

b = BytesBuilder.new!()
b.push!(65u8)
b.finish!() => "A".to_bytes()

extend!

def extend!(self, b: bytes) -> ()

Append every byte of b, in order, to the end of the buffer.

b = BytesBuilder.new!()
b.extend!("hi".to_bytes())
b.finish!() => "hi".to_bytes()

reserve!

def reserve!(self, n: int) -> ()

Make room for n more bytes up front, which lets a run of pushes grow the buffer once. It is a pure hint: it changes no byte the builder has and no byte finish! returns, only when the allocation happens. Reserving less than the room already there does nothing.

The buffer grows on its own regardless, and this is worth reaching for only where the final size is known ahead of time: a pixel or vertex buffer, a frame whose header states its length.

A request too large to satisfy is declined and never fatal: the buffer is left as it was, and the pushes that follow grow it the ordinary way. Asking for room is never how a program dies.

b = BytesBuilder.new!()
b.reserve!(64)
b.spare_capacity!() >= 64 => true
b.finish!() => "".to_bytes()
b = BytesBuilder.new!()
b.reserve!(9223372036854775807)
b.spare_capacity!() => 0

spare_capacity!

def spare_capacity!(self) -> int

Room already allocated beyond the bytes written: how much can still be pushed before the buffer grows again.

The figure past a reserve! is the allocator's business, and the only promise is >= n after reserve!(n). Compare it, don't pin it.

b = BytesBuilder.new!()
b.spare_capacity!() => 0

pushbeu16!

def push_be_u16!(self, n: u16) -> ()

Append n as 2 bytes, big-endian, high byte first, in network byte order). Pure Hanki over push!; the reader half is BytesReader.take_be_u16!. Little-endian mates are absent until a caller needs one.

b = BytesBuilder.new!()
b.push_be_u16!(16706u16)
b.finish!() => "AB".to_bytes()

pushbeu32!

def push_be_u32!(self, n: u32) -> ()

Append n as 4 bytes, big-endian, high byte first, in network byte order). The reader half is BytesReader.take_be_u32!.

b = BytesBuilder.new!()
b.push_be_u32!(1094861636u32)
b.finish!() => "ABCD".to_bytes()

pushbeu64!

def push_be_u64!(self, n: u64) -> ()

Append n as 8 bytes, big-endian, high byte first, in network byte order). The reader half is BytesReader.take_be_u64!.

b = BytesBuilder.new!()
b.push_be_u64!(4702394921427289928u64)
b.finish!() => "ABCDEFGH".to_bytes()

pushleu16!

def push_le_u16!(self, n: u16) -> ()

Append n as 2 bytes, little-endian (low byte first). The reader half is BytesReader.take_le_u16!.

b = BytesBuilder.new!()
b.push_le_u16!(16961u16)
b.finish!() => "AB".to_bytes()

pushleu32!

def push_le_u32!(self, n: u32) -> ()

Append n as 4 bytes, little-endian (low byte first). The reader half is BytesReader.take_le_u32!.

b = BytesBuilder.new!()
b.push_le_u32!(1145258561u32)
b.finish!() => "ABCD".to_bytes()

pushleu64!

def push_le_u64!(self, n: u64) -> ()

Append n as 8 bytes, little-endian (low byte first). The reader half is BytesReader.take_le_u64!.

b = BytesBuilder.new!()
b.push_le_u64!(5208208757389214273u64)
b.finish!() => "ABCDEFGH".to_bytes()

pushbef32!

def push_be_f32!(self, x: f32) -> ()

Append x as its 4 IEEE-754 bits, big-endian. Bit-exact: the value goes through f32.to_bits, and a NaN retains its payload and its signalling bit unnormalised.

The example writes its float as from_bits because the assertion is about bytes: a float whose encoding is four printable ASCII bytes is the only kind a string literal can compare against, since a bytes built from a string has that string's UTF-8 and 0x80 alone is not valid UTF-8.

b = BytesBuilder.new!()
b.push_be_f32!(f32.from_bits(1094861636u32))
b.finish!() => "ABCD".to_bytes()

pushlef32!

def push_le_f32!(self, x: f32) -> ()

Append x as its 4 IEEE-754 bits, little-endian, the layout a GPU vertex buffer and every mainstream CPU expect, and packing floats therefore usually wants this one and not the big-endian sibling.

b = BytesBuilder.new!()
b.push_le_f32!(f32.from_bits(1145258561u32))
b.finish!() => "ABCD".to_bytes()

pushbef64!

def push_be_f64!(self, x: f64) -> ()

Append x as its 8 IEEE-754 bits, big-endian. Bit-exact, like the f32 pair.

b = BytesBuilder.new!()
b.push_be_f64!(f64.from_bits(4702394921427289928u64))
b.finish!() => "ABCDEFGH".to_bytes()

pushlef64!

def push_le_f64!(self, x: f64) -> ()

Append x as its 8 IEEE-754 bits, little-endian.

b = BytesBuilder.new!()
b.push_le_f64!(f64.from_bits(5208208757389214273u64))
b.finish!() => "ABCDEFGH".to_bytes()

finish!

def finish!(self) -> bytes

The bytes accumulated so far, as an immutable bytes. Non-consuming: the builder is left intact and may go on growing, and a later finish! returns these bytes plus whatever was pushed in between.

b = BytesBuilder.new!()
b.push!(72u8)
b.extend!("i!".to_bytes())
b.finish!() => "Hi!".to_bytes()