deserializer
stdlib/core/deserializer.hk: the Deserializer format abstraction and the built-in binary deserializer.
Deserializer is the read side of the format-generic serialization framework (HANKI.md §16), the inverse of Serializer: each method reads one structural event from a source and advances it, returning a DecodeError (with the byte offset) on truncation or a bad tag. Decode impls name the events and stay format-agnostic.
BinaryDeserializer reads the built-in fixed-width binary format over a BytesReader cursor, the inverse of BinarySerializer. The cursor tracks position, and no offset is threaded by hand. take_struct! reads nothing (a struct's field count is static); a framed format would read and check an array header instead.
Every read is total: a short read becomes Err(Truncated), never a crash. Each method is an action (!) with an empty effect row, like the BytesReader it drives (HANKI.md §4).
DecodeError
type DecodeError
Truncated(int)
BadTag(u8, int)
BadUtf8(int)
TooDeep(int)
InvalidValue(validation.ValidationError, int)
InvalidByteLimit(int, int)
ByteLimitExceeded(int, int, int)
InvalidBodyHeader(int)
Rejected(string, string, int)
end
Why a decode failed, with the byte offset at which it was detected.
BodyKind
type BodyKind
StringBody
BytesBody
end
The wire body's interpretation. Both lengths count bytes, including UTF-8.
impl Eq<BodyKind>
eq?
def eq?(self, other: BodyKind) -> bool
Compare the interpretation of two wire bodies. @no-doctest: exercised by checked body kind validation
BodyHeader
opaque BodyHeader
count: int
start: int
body_kind: BodyKind
checkpoint: ReadCheckpoint
end
Metadata and a one-use checkpoint; no body bytes or reader are retained.
impl BodyHeader
byte_length
prop byte_length(self) -> int
The advertised body length in bytes, without checking body availability. @no-doctest: exercised by decoder header tests
offset
prop offset(self) -> int
The first byte of the wire header. @no-doctest: exercised by decoder header tests
kind
prop kind(self) -> BodyKind
Whether this is a UTF-8 string body or uninterpreted bytes. @no-doctest: exercised by decoder header tests
new!
def new!(src: BytesReader, length: u64, offset: int, kind: BodyKind) -> Result<BodyHeader, DecodeError>
Format implementors issue this after parsing their length header. The length remains exact through the full u64 range. The start must be a finite offset between zero and the current cursor. Replaces any earlier header. @no-doctest: exercised through both format implementations
_claim!
def _claim!(self, src: BytesReader, kind: BodyKind) -> Result<(), DecodeError>
take_bytes!
def take_bytes!(self, src: BytesReader) -> Result<bytes, DecodeError>
Read a checked bytes body into detached storage. Failed body attempts consume a valid checkpoint; a kind or identity mismatch does not. @no-doctest: exercised by decoder body tests
take_string!
def take_string!(self, src: BytesReader) -> Result<string, DecodeError>
Read and validate UTF-8 after claiming the header. Bad UTF-8 consumes the body and reports its header's start; a short body remains untouched. @no-doctest: exercised by decoder body tests
skip!
def skip!(self, src: BytesReader) -> Result<(), DecodeError>
Skip this body's bytes without reading them or validating UTF-8. A short body returns Truncated without advancing and consumes the header. @no-doctest: exercised by selective decoding tests
Deserializer
trait Deserializer
take_bool!
def take_bool!(self) -> Result<bool, DecodeError>
Reads one bool from the input.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_u8!
def take_u8!(self) -> Result<u8, DecodeError>
Reads one u8 from the input.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_u16!
def take_u16!(self) -> Result<u16, DecodeError>
Reads one u16 from the input.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_u32!
def take_u32!(self) -> Result<u32, DecodeError>
Reads one u32 from the input.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_u64!
def take_u64!(self) -> Result<u64, DecodeError>
Reads one u64 from the input.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_i32!
def take_i32!(self) -> Result<i32, DecodeError>
Reads one i32 from the input.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_i64!
def take_i64!(self) -> Result<i64, DecodeError>
Reads one i64 from the input.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_f64!
def take_f64!(self) -> Result<f64, DecodeError>
Reads one f64 from the input.
@no-doctest: structural decode op; the round-trip tests below exercise it
takestringheader!
def take_string_header!(self) -> Result<BodyHeader, DecodeError>
Read only the string header. Never inspect, copy, or retain its body. @no-doctest: exercised by format header tests
takestringbody!
def take_string_body!(self, header: BodyHeader) -> Result<string, DecodeError>
Consume a matching one-use header and materialize its body. @no-doctest: exercised by format body tests
take_string!
def take_string!(self) -> Result<string, DecodeError>
Read one string with no application byte limit. @no-doctest: exercised by format round-trip tests
takestringbounded!
def take_string_bounded!(self, max_bytes: int) -> Result<string, DecodeError>
Reject an advertised body larger than max_bytes before accessing it. Negative and non-finite limits fail before reading the header. Zero permits empty bodies. Header errors precede size errors; size errors precede body truncation and UTF-8 validation. Rejection leaves the cursor immediately after the header. The limit applies independently to each field. @no-doctest: exercised by bounded decoder tests
takebytesheader!
def take_bytes_header!(self) -> Result<BodyHeader, DecodeError>
Read only the bytes header. Never inspect, copy, or retain its body. @no-doctest: exercised by format header tests
takebytesbody!
def take_bytes_body!(self, header: BodyHeader) -> Result<bytes, DecodeError>
Consume a matching one-use header and materialize its body. @no-doctest: exercised by format body tests
take_bytes!
def take_bytes!(self) -> Result<bytes, DecodeError>
Read one bytes with no application byte limit. @no-doctest: exercised by format round-trip tests
takebytesbounded!
def take_bytes_bounded!(self, max_bytes: int) -> Result<bytes, DecodeError>
Reject an advertised body larger than max_bytes before accessing it. Negative and non-finite limits fail before reading the header. Zero permits empty bodies. Header errors precede size errors; size errors precede body truncation and UTF-8 validation. Rejection leaves the cursor immediately after the header. The limit applies independently to each field. @no-doctest: exercised by bounded decoder tests
skip_body!
def skip_body!(self, header: BodyHeader) -> Result<(), DecodeError>
Skip a checked string or bytes body without materializing it. @no-doctest: exercised by selective decoding tests
takeissome!
def take_is_some!(self) -> Result<bool, DecodeError>
true if a Some payload follows, false for None. @no-doctest: structural decode op; the round-trip tests below exercise it
take_seq!
def take_seq!(self) -> Result<int, DecodeError>
The element / pair count of a sequence or map. @no-doctest: structural decode op; the round-trip tests below exercise it
take_map!
def take_map!(self) -> Result<int, DecodeError>
Reads a map header, returning its length.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_struct!
def take_struct!(self, fields: int) -> Result<(), DecodeError>
Read (and, for a framed format, check) a struct header of fields fields. @no-doctest: structural decode op; the round-trip tests below exercise it
take_variant!
def take_variant!(self, payload_counts: List<int>) -> Result<u8, DecodeError>
A sum's variant tag. payload_counts gives each tag's payload arity so a framed format can verify the selected variant's frame. @no-doctest: structural decode op; the round-trip tests below exercise it
position!
def position!(self) -> int
The current read offset, for locating a DecodeError (e.g. the byte a bad variant tag was read from). @no-doctest: structural decode op; the round-trip tests below exercise it
remaining!
def remaining!(self) -> int
Unread bytes left in the source. Standard collections cap their local element or pair count against this before looping. Custom element decoders can consume zero bytes; the cap supplies no cumulative decode budget. @no-doctest: structural decode op; the round-trip tests below exercise it
take_int!
def take_int!(self) -> Result<int, DecodeError>
-- the exact tier ------------------------------------------------------
The read side of Serializer's exact-tier events, in the same shape and for the same reason: each has a default body over the events above, and a Deserializer written before they existed goes on compiling. take_int! is the ground case; the other two reduce to it through the exact decomposition, sentinels included. Reads one arbitrary-precision int written by put_int!: the class byte, then the digits when it says finite.
A class byte outside 0 ..= 3 is BadTag, and digits int.parse rejects are BadTag at the same offset. The payload is structurally a string, and Truncated/BadUtf8 are already spoken for by take_string!.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_decimal!
def take_decimal!(self) -> Result<decimal, DecodeError>
Reads one decimal from the unscaled/scale pair put_decimal! wrote.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_rational!
def take_rational!(self) -> Result<rational, DecodeError>
Reads one rational from the numerator/denominator pair put_rational! wrote.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_f32!
def take_f32!(self) -> Result<f32, DecodeError>
Reads one f32 from the IEEE bit pattern put_f32! wrote. Every 32-bit pattern is some f32, and this cannot fail beyond the underlying read.
@no-doctest: structural decode op; the round-trip tests below exercise it
intsentinel
def _int_sentinel(class: u8) -> Option<int>
The inverse of serializer._int_class for the three sentinel classes; None for a class byte that is neither a sentinel nor the finite 0.
BinaryDeserializer
struct BinaryDeserializer
src: BytesReader
end
The built-in fixed-width binary format over a BytesReader cursor.
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.
detake!
def _de_take!(src: BytesReader, n: int) -> Result<bytes, DecodeError>
deheader!
def _de_header!(src: BytesReader, kind: BodyKind) -> Result<BodyHeader, DecodeError>
Parse only the u32 big-endian length prefix.
beu16
def _be_u16(b: bytes) -> u16
beu32
def _be_u32(b: bytes) -> u32
beu64
def _be_u64(b: bytes) -> u64
deu32!
def _de_u32!(src: BytesReader) -> Result<u32, DecodeError>
u32 read shared by length prefixes, counts, and the public take_u32!.
deu64!
def _de_u64!(src: BytesReader) -> Result<u64, DecodeError>
u64 read shared by the public take_u64! / take_i64! / take_f64!.
impl Deserializer<BinaryDeserializer>
take_bool!
def take_bool!(self) -> Result<bool, DecodeError>
Reads one bool from the input.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_u8!
def take_u8!(self) -> Result<u8, DecodeError>
Reads one u8 from the input.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_u16!
def take_u16!(self) -> Result<u16, DecodeError>
Reads one u16 from the input.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_u32!
def take_u32!(self) -> Result<u32, DecodeError>
Reads one u32 from the input.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_u64!
def take_u64!(self) -> Result<u64, DecodeError>
Reads one u64 from the input.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_i32!
def take_i32!(self) -> Result<i32, DecodeError>
Reads one i32 from the input.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_i64!
def take_i64!(self) -> Result<i64, DecodeError>
Reads one i64 from the input.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_f64!
def take_f64!(self) -> Result<f64, DecodeError>
Reads one f64 from the input.
@no-doctest: structural decode op; the round-trip tests below exercise it
takestringheader!
def take_string_header!(self) -> Result<BodyHeader, DecodeError>
Read only a binary string length prefix. @no-doctest: exercised by the generic header tests
takestringbody!
def take_string_body!(self, header: BodyHeader) -> Result<string, DecodeError>
Consume a checked binary string body. @no-doctest: exercised by the generic body tests
takebytesheader!
def take_bytes_header!(self) -> Result<BodyHeader, DecodeError>
Read only a binary bytes length prefix. @no-doctest: exercised by the generic header tests
takebytesbody!
def take_bytes_body!(self, header: BodyHeader) -> Result<bytes, DecodeError>
Consume a checked binary bytes body. @no-doctest: exercised by the generic body tests
skip_body!
def skip_body!(self, header: BodyHeader) -> Result<(), DecodeError>
Skip a checked binary body without accessing its bytes. @no-doctest: exercised by the generic skip tests
takeissome!
def take_is_some!(self) -> Result<bool, DecodeError>
Reads an optional's presence flag; the value follows when true.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_seq!
def take_seq!(self) -> Result<int, DecodeError>
Reads a seq header, returning its length.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_map!
def take_map!(self) -> Result<int, DecodeError>
Reads a map header, returning its length.
@no-doctest: structural decode op; the round-trip tests below exercise it
take_struct!
def take_struct!(self, fields: int) -> Result<(), DecodeError>
A struct's field count is static, and the binary format reads no framing. @no-doctest: structural decode op; the round-trip tests below exercise it
take_variant!
def take_variant!(self, _payload_counts: List<int>) -> Result<u8, DecodeError>
Reads a sum variant's tag; its payload follows. The binary format has no enclosing frame; the derived payload-count table is unused.
@no-doctest: structural decode op; the round-trip tests below exercise it
position!
def position!(self) -> int
The current read offset, in bytes.
@no-doctest: structural decode op; the round-trip tests below exercise it
remaining!
def remaining!(self) -> int
How many bytes remain unread.
@no-doctest: structural decode op; the round-trip tests below exercise it