hanki

bytes_reader

stdlib/core/bytes_reader.hk: the BytesReader cursor over bytes.

BytesReader is a runtime-managed, non-sendable native resource (HANKI.md §4): a forward cursor over an immutable bytes buffer, the read-side mirror of BytesBuilder. A decoder reads through it and the cursor advances in place, and no byte offset has to be threaded by hand.

Every operation is an action (!) with an empty effect row. As with BytesBuilder, the ! is the referential-transparency boundary and no real-world capability: take! mutates the cursor, and even remaining! / position! read state that a prior take! changed, and neither is substitutable. The mutation never escapes into the value model.

take! is clamped and never checked: it yields up to n bytes (fewer at the end of the buffer) so the primitives stay total. A caller turns a short read into a typed error by comparing the returned length to n, using position! for the failure offset.

The fixed-width reads (take_be_u16! and friends) answer Option rather than clamping, there being no such thing as a partial integer: a short buffer gives None and leaves the cursor where it was, and the tail remains readable with take!. They follow peek!'s form and never take!'s.

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 advances the boxed cursor in place.

BytesReader

BytesReader, or bytes.BytesReader, 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 BytesReader

new!

def new!(input: bytes) -> BytesReader

A new cursor at offset 0 over input.

r = BytesReader.new!("AB".to_bytes())
r.remaining!() => 2

take!

def take!(self, n: int) -> bytes

Take up to n bytes from the cursor, advancing past them. Yields fewer than n only at the end of the buffer (and none for a negative n).

r = BytesReader.new!("ABC".to_bytes())
r.take!(2) => "AB".to_bytes()

remaining!

def remaining!(self) -> int

The number of bytes not yet taken.

r = BytesReader.new!("AB".to_bytes())
r.take!(1)
r.remaining!() => 1

position!

def position!(self) -> int

The cursor's current offset from the start of the buffer.

r = BytesReader.new!("AB".to_bytes())
r.take!(1)
r.position!() => 1

peek!

def peek!(self) -> Option<u8>

The next byte without advancing: Some(byte), or None at the end. A self-describing decoder peeks to branch before committing a read (e.g. CBOR null vs an Option's value). Returns Option<u8> like bytes.get, It is no -1 sentinel, under the no-nulls-anywhere invariant (HANKI.md §20).

r = BytesReader.new!("AB".to_bytes())
r.peek!() => Some(65u8)

takebeu16!

def take_be_u16!(self) -> Option<u16>

The next 2 bytes as a big-endian u16, advancing past them. None when fewer than 2 remain, and then the cursor does not advance and the tail remains readable with take!. The reader half of BytesBuilder.push_be_u16!.

r = BytesReader.new!("AB".to_bytes())
r.take_be_u16!() => Some(16706u16)
r.take_be_u16!() => None

takebeu32!

def take_be_u32!(self) -> Option<u32>

The next 4 bytes as a big-endian u32, advancing past them. None when fewer than 4 remain, and then the cursor does not advance.

r = BytesReader.new!("ABCD".to_bytes())
r.take_be_u32!() => Some(1094861636u32)
r.take_be_u32!() => None

takebeu64!

def take_be_u64!(self) -> Option<u64>

The next 8 bytes as a big-endian u64, advancing past them. None when fewer than 8 remain, and then the cursor does not advance.

r = BytesReader.new!("ABCDEFGH".to_bytes())
r.take_be_u64!() => Some(4702394921427289928u64)
r.take_be_u64!() => None

takeleu16!

def take_le_u16!(self) -> Option<u16>

The next 2 bytes as a little-endian u16, advancing past them. None when fewer than 2 remain, and then the cursor does not advance.

r = BytesReader.new!("AB".to_bytes())
r.take_le_u16!() => Some(16961u16)
r.take_le_u16!() => None

takeleu32!

def take_le_u32!(self) -> Option<u32>

The next 4 bytes as a little-endian u32, advancing past them. None when fewer than 4 remain, and then the cursor does not advance.

r = BytesReader.new!("ABCD".to_bytes())
r.take_le_u32!() => Some(1145258561u32)
r.take_le_u32!() => None

takeleu64!

def take_le_u64!(self) -> Option<u64>

The next 8 bytes as a little-endian u64, advancing past them. None when fewer than 8 remain, and then the cursor does not advance.

r = BytesReader.new!("ABCDEFGH".to_bytes())
r.take_le_u64!() => Some(5208208757389214273u64)
r.take_le_u64!() => None

takebef32!

def take_be_f32!(self) -> Option<f32>

The next 4 bytes as a big-endian f32, advancing past them. Bit-exact: the bytes go through f32.from_bits, and a NaN arrives with the payload it was written with. None when fewer than 4 remain.

The example asserts on to_bits and not on the float, which is the right comparison for a bit-exact read and also the only one available: the float tier has no Eq impl, and two f32s do not compare with ==.

r = BytesReader.new!("ABCD".to_bytes())
r.take_be_f32!().map(|x: f32| x.to_bits()) => Some(1094861636u32)

takelef32!

def take_le_f32!(self) -> Option<f32>

The next 4 bytes as a little-endian f32 - the layout a GPU vertex buffer uses. Bit-exact, like the big-endian sibling.

r = BytesReader.new!("ABCD".to_bytes())
r.take_le_f32!().map(|x: f32| x.to_bits()) => Some(1145258561u32)

takebef64!

def take_be_f64!(self) -> Option<f64>

The next 8 bytes as a big-endian f64, advancing past them. Bit-exact.

r = BytesReader.new!("ABCDEFGH".to_bytes())
r.take_be_f64!().map(|x: f64| x.to_bits()) => Some(4702394921427289928u64)

takelef64!

def take_le_f64!(self) -> Option<f64>

The next 8 bytes as a little-endian f64. Bit-exact.

r = BytesReader.new!("ABCDEFGH".to_bytes())
r.take_le_f64!().map(|x: f64| x.to_bits()) => Some(5208208757389214273u64)