bytes
stdlib/bytes.hk: methods on the bytes type.
bytes is an immutable contiguous byte buffer (HANKI.md §4): the raw counterpart to string, for binary I/O and codecs. It shares the string heap layout, and has no UTF-8 guarantee. The primitive operations are @intrinsic; lowering emits Op::HostCall and the scheduler dispatches into the registered Rust closure. All offsets are byte offsets.
Surface: b.length b.slice(start, stop) b.join(parts) b.get(i) b.tostring() b.empty? b.any? / all? / position / find / fold / each! / tryfold! / try_each!
The scan combinators are pure Hanki over get and length, named as List's are so a byte scan reads like any other. They exist because a buffer with no way to iterate leaves every scan a hand-rolled index loop, which is what this type's callers were all writing. Transforms (map, filter) are absent: a byte-to-byte map and a byte-to-List<T> map are different operations, BytesBuilder already owns construction, and neither question needed answering to unblock scanning. Further ! twins land when a caller needs one.
Construction is by encoding a string (string.to_bytes) or from a codec; there are no bytes literals (HANKI.md §17). Equality is byte-wise (Op::EqBytes), surfaced as impl Eq<bytes> in eq.hk.
Utf8Error
struct Utf8Error
valid_up_to: int
end
The byte offset at which a UTF-8 decode first failed. Carried by the Err arm of bytes.to_string so a caller can report or recover at the split point, in place of learning only that decoding failed.
impl Eq<Utf8Error>
Utf8Error is Eq so a Result<string, Utf8Error> (what to_string returns) can be compared with ==. A hand-written impl and no @derive: the baked-stdlib-bytecode prefix assumes stdlib expansion adds no items, so a derived impl would break it.
eq?
def eq?(self, other: Self) -> bool
Equal when both errors report the same byte offset.
@no-doctest: a Utf8Error arises only from decoding invalid UTF-8 bytes (no inline byte literal exists), and a self-contained eq? example cannot construct one
impl bytes
length
prop length(self) -> int
The length in bytes.
"abc".to_bytes().length => 3
"".to_bytes().length => 0
slice
def slice(self, start: int, stop: int) -> bytes
The sub-buffer over the byte range [start, stop). Offsets are clamped to [0, length()]; a negative start clamps to 0, a stop past the buffer clamps to the length, and stop <= start yields an empty bytes. Raw byte offsets: unlike string.slice, there is no codepoint rounding. It is stop and never end, end being a keyword.
"hello".to_bytes().slice(0, 2) => "he".to_bytes()
"hello".to_bytes().slice(3, 1) => "".to_bytes()
join
def join(self, parts: List<bytes>) -> bytes
The pieces fused into one buffer, self between each pair, the replacement for the concat chain this type used to carry.
parts: List<bytes> = List.empty().append("a".to_bytes()).append("b".to_bytes())
"-".to_bytes().join(parts) => "a-b".to_bytes()
"".to_bytes().join(parts) => "ab".to_bytes()
One pass over the pieces, appending into a single buffer, and fusing n of them is O(total bytes). A concat chain was O(n²): every link copied everything to its left again. That is the whole reason concat is gone and this is here; see str.join, which does the same for text.
get
def get(self, i: int) -> Option<u8>
The byte at offset i, or None when i is out of range. A negative i is always None.
"AB".to_bytes().get(0) => Some(65u8)
"AB".to_bytes().get(9) => None
get_or
def get_or(self, i: int, fallback: u8) -> u8
The byte at offset i, or fallback when i is out of bounds.
b.get_or(i, d) is b.get(i).unwrap_or(d) without the Option, which get would otherwise allocate on the heap for every read. A byte scan is the case that notices.
"AB".to_bytes().get_or(0, 0u8) => 65u8
"AB".to_bytes().get_or(9, 0u8) => 0u8
to_string
def to_string(self) -> Result<string, Utf8Error>
Decode the buffer as UTF-8. Ok(s) when the bytes are valid UTF-8; Err(Utf8Error) carrying the first invalid offset otherwise.
"hi".to_bytes().to_string() => Ok("hi")
hash_u64
def hash_u64(self) -> u64
A 64-bit hash of the bytes. Backs Hash<bytes>; equal buffers hash equal.
"hi".to_bytes().hash_u64() == "hi".to_bytes().hash_u64() => true
compare
def compare(self, other: bytes) -> i32
Lexicographic comparison: a negative i32 when self sorts before other, 0 when equal, a positive one when after. The cross-tier primitive Ord<bytes>.cmp maps onto Less / Equal / Greater (and Ord<string> rides on it via to_bytes).
"a".to_bytes().compare("b".to_bytes()) => -1i32
"b".to_bytes().compare("b".to_bytes()) => 0i32
empty?
prop empty?(self) -> bool
True where self has no bytes. b.empty? is b.length == 0.
"".to_bytes().empty? => true
"abc".to_bytes().empty? => false
any?
def any?(self, pred: (u8) -> bool) -> bool
Whether any byte satisfies pred. Stops at the first that does.
"hi".to_bytes().any?(|b| b == 104u8) => true
"hi".to_bytes().any?(|b| b == 122u8) => false
"".to_bytes().any?(|b| true) => false
all?
def all?(self, pred: (u8) -> bool) -> bool
Whether every byte satisfies pred. Stops at the first that does not, and an empty buffer satisfies it vacuously, as List.all? does.
"hi".to_bytes().all?(|b| b > 90u8) => true
"hi".to_bytes().all?(|b| b > 104u8) => false
"".to_bytes().all?(|b| false) => true
position
def position(self, pred: (u8) -> bool) -> Option<int>
The offset of the first byte satisfying pred, or None.
The one a scan usually wants: find hands back a u8 the predicate already characterized, while the position is what a caller slices at.
"hi".to_bytes().position(|b| b == 105u8) => Some(1)
"hi".to_bytes().position(|b| b == 122u8) => None
find
def find(self, pred: (u8) -> bool) -> Option<u8>
The first byte satisfying pred, or None. Mirrors List.find; reach for position when what you need is where it is.
"hi".to_bytes().find(|b| b > 100u8) => Some(104u8)
"hi".to_bytes().find(|b| b > 200u8) => None
fold
def fold<U>(self, init: U, f: (U, u8) -> U) -> U
Left fold over the bytes, in order.
"hi".to_bytes().fold(0u32, |acc, b| acc + b.to_u32()) => 209u32
"".to_bytes().fold(7, |acc, b| acc + 1) => 7
each!
def each!(self, f: (u8) -> () [e]) -> () [e]
Perform f on each byte, in order. The effect-polymorphic form, and the callback may do anything the caller may.
"hi".to_bytes().each!(|b| ())
try_fold!
def try_fold!<U, E>(self, init: U, f: (U, u8) -> Result<U, E> [e]) -> Result<U, E> [e]
fold with an effectful step that can stop: the first Err ends the traversal as the result. Later bytes are not visited, and their effects never happen. An empty buffer gives Ok(init).
"hi".to_bytes().try_fold!(0u32, |acc: u32, b: u8| Ok(acc + b.to_u32())) => Ok(209u32)
"hi".to_bytes().try_fold!(0u32, |acc: u32, b: u8| Err("no")) => Err("no")
"".to_bytes().try_fold!(7, |acc: int, b: u8| Err("never")) => Ok(7)
try_each!
def try_each!<E>(self, f: (u8) -> Result<(), E> [e]) -> Result<(), E> [e]
each! that can stop: f returns Result<(), E>, and the first Err ends the traversal as the result; later bytes are not visited.
"hi".to_bytes().try_each!(|b: u8| if b == 104u8 then Err("h") else Ok(())) => Err("h")
"hi".to_bytes().try_each!(|b: u8| if b > 0u8 then Ok(()) else Err("nul")) => Ok(())