Decoder

:= {
    limits : Limits,
    offset : U64,
    stack : List(ArrayFrame),
    state : ParserState,
    value_count : U64,
}

A genuinely incremental, platform-independent RESP2 decoder.

The decoder retains parsed state rather than a copy of the incomplete wire frame. Each input byte is examined once. Progress.values may contain zero, one, or several complete top-level replies, so transport reads and RESP values never need to share boundaries.

default_limits : Limits

Conservative per-reply defaults, shared with Config.default. These are protocol limits, not a heap or aggregate exchange budget. Direct callers must also bound input chunks and the total replies/bytes they retain.

redis_compatible_limits : Limits

Explicit larger policy for Redis-sized values. Not an exchange or heap limit; use only with caller-owned aggregate and input-chunk budgets.

with_limits : Limits -> Decoder

Construct an empty decoder with caller-selected limits.

buffered_len : Decoder -> U64

Bytes consumed into the incomplete top-level frame. This is a logical frame offset; the decoder does not retain all of those raw bytes.

feed : Decoder, List(U8) -> FeedResult

Decode every complete top-level RESP2 value in chunk. Per-reply limits reset between values; this does not limit aggregate chunk/output size. Execute supplies read-size and cumulative exchange limits for clients.

finish : Decoder -> Try({  }, DecodeError)

Assert that the stream ended between frames rather than inside one.

Limits : {
    max_array_length : U64,
    max_bulk_length : U64,
    max_depth : U64,
    max_frame_length : U64,
    max_line_length : U64,
    max_values : U64,
}

Resource limits applied independently to each top-level RESP value. Limits are invariant to how the transport fragments that value. max_frame_length includes every wire byte in the value, while max_line_length counts only bytes between a type prefix and CRLF. max_values counts an aggregate itself plus all of its descendants. max_depth counts array prefixes (including empty and null arrays), so a top-level array has depth one. Array and bulk limits count immediate elements and payload bytes, respectively.

EndContext : [
    ArrayItems({ remaining : U64 }),
    BulkCarriageReturn,
    BulkData({ remaining : U64 }),
    BulkLineFeed,
    LineEnd,
    LineFeed,
]

What the decoder was waiting for when the byte stream ended.

DecodeError : [
    ArrayLengthLimitExceeded({ actual : U64, at : U64, limit : U64 }),
    BulkLengthLimitExceeded({ actual : U64, at : U64, limit : U64 }),
    ExpectedBulkTerminator({ actual : U8, at : U64, expected : U8 }),
    FrameLengthLimitExceeded({ at : U64, limit : U64 }),
    InvalidArrayLength({ at : U64, header : List(U8) }),
    InvalidBulkLength({ at : U64, header : List(U8) }),
    InvalidInteger({ at : U64, header : List(U8) }),
    InvalidLineEnding({ at : U64 }),
    LineLengthLimitExceeded({ at : U64, limit : U64 }),
    NestingLimitExceeded({ at : U64, limit : U64 }),
    UnexpectedEnd({ at : U64, context : EndContext }),
    UnknownType({ at : U64, byte : U8 }),
    ValueLimitExceeded({ at : U64, limit : U64 }),
]

A malformed or resource-exhausting RESP frame. Every at offset is relative to the start of the current top-level frame.

FeedResult : [
    Failed({ completed : List(Resp), error : DecodeError }),
    Progress({ decoder : Decoder, values : List(Resp) }),
]

The result of feeding a chunk. A failure also returns any complete top-level values which preceded the malformed frame in the same input. A failed decoder is terminal and must not be reused.