codec.pony

primitive InsufficientData
  """
  Returned when a decode operation needs more bytes than are available.

  In a streaming context, this means "wait for more data." It is not an
  error in the data itself — the input is simply incomplete.
  """
  fun string(): String iso^ => "Insufficient data".clone()

class val MalformedData
  """
  Returned when a decode operation encounters structurally invalid data.

  Unlike `InsufficientData`, this indicates the data itself is wrong — a
  literal mismatch, an unknown tag, or a value that violates a protocol
  constraint. No amount of additional data will fix it.
  """
  let _message: String
  let _bit_offset: USize

  new val create(message': String, bit_offset': USize = 0) =>
    _message = message'
    _bit_offset = bit_offset'

  fun message(): String => _message
  fun bit_offset(): USize => _bit_offset

  fun string(): String iso^ =>
    (_message + " at bit offset " + _bit_offset.string()).clone()

type DecodeError is (InsufficientData | MalformedData)
  """
  The error union returned by `decode`: either the data ran out
  (`InsufficientData`) or it was structurally wrong (`MalformedData`).
  """

class val EncodeViolation
  """
  Returned when an encode operation fails — typically because a value does
  not fit the codec's constraints (e.g., a U8 value exceeding a 4-bit field)
  or because the output is not byte-aligned.
  """
  let _message: String

  new val create(message': String) =>
    _message = message'

  fun message(): String => _message
  fun string(): String iso^ => _message.clone()

type EncodeError is EncodeViolation
  """
  The error type returned by `encode`.
  """

trait val Codec[A: Any val]
  """
  A bidirectional codec that decodes bytes into a value and encodes a value
  into bytes.

  Codecs are `val` — immutable and shareable across actors. Small codecs
  compose into larger ones via combinators like `Seq2`, `Mapped`, and
  `FlatZip`.

  `decode` takes an array and a bit offset, returning either the decoded
  value paired with the number of bits consumed, or a `DecodeError`. The
  array is `val` so byte-slice codecs like `Bytes` can return zero-copy
  views via `Array.trim`.

  `encode` converts a value to bytes, returning either the encoded bytes or
  an `EncodeError`.
  """
  fun decode(data: Array[U8] val, offset: USize)
    : ((A, USize) | DecodeError)
    """
    Decode a value from `data` starting at the given bit offset.
    """

  fun encode(value: A): (Array[U8] val | EncodeError) =>
    let writer = _BitWriter
    match \exhaustive\ _encode_bits(value, writer)
    | let w: _BitWriter ref =>
      try w.done()?
      else EncodeViolation("Non-byte-aligned output")
      end
    | let e: EncodeError => e
    end

  fun _encode_bits(value: A, writer: _BitWriter ref)
    : (_BitWriter ref | EncodeError)