Erlang term decoder

primitive ErlangTermDecoder
  """
  Reads terms out of an `EMessage`.

  `decode` is the usual entry point.  Decoding either returns a complete term
  or `None`: a half decoded term is never handed out.
  """

  fun decode(message: EMessage ref): (ErlangTerm | None) =>
    (let term, _) = decode_at(message, message.beginning)
    term

  fun decode_at(message: EMessage ref, position: I32)
    : ((ErlangTerm | None), I32)
  =>
    (let type_tag, _) = message.type_at(position)

    match type_tag
    | TermType.none() => (None, position)
    | TermType.atom() | TermType.small_atom() | TermType.atom_utf8()
    | TermType.small_atom_utf8()
    =>
      _decode_atom(message, position)
    | TermType.small_integer() | TermType.integer() =>
      _decode_int(message, position)
    | TermType.small_big() | TermType.large_big() =>
      _decode_int_or_big_integer(message, position, type_tag)
    | TermType.float() | TermType.new_float() =>
      _decode_float(message, position)
    | TermType.binary() => _decode_binary(message, position)
    | TermType.bit_binary() => _decode_bitstring(message, position)
    | TermType.string() => _decode_string(message, position)
    | TermType.nil() =>
      (ErlangList(recover Array[ErlangTerm] end), position + 1)
    | TermType.list() => _decode_list(message, position)
    | TermType.small_tuple() | TermType.large_tuple() =>
      _decode_tuple(message, position)
    | TermType.map() => _decode_map(message, position)
    | TermType.pid() | TermType.new_pid() => _decode_pid(message, position)
    | TermType.port() | TermType.new_port() | TermType.v4_port() =>
      _decode_port(message, position)
    | TermType.reference() | TermType.new_reference()
    | TermType.newer_reference()
    =>
      _decode_reference(message, position)
    else
      _decode_verbatim(message, position, type_tag)
    end

  fun _decode_atom(message: EMessage ref, position: I32)
    : ((ErlangTerm | None), I32)
  =>
    (let name, let next) = message.decode_atom(position)
    match name
    | let atom: String => (ErlangAtom(atom), next)
    else (None, position)
    end

  fun _decode_int(message: EMessage ref, position: I32)
    : ((ErlangTerm | None), I32)
  =>
    (let value, let next) = message.decode_int(position)
    match value
    | let integer: I64 => (ErlangInteger(integer), next)
    else (None, position)
    end

  fun _decode_int_or_big_integer(message: EMessage ref, position: I32,
    type_tag: I32)
    : ((ErlangTerm | None), I32)
  =>
    match _decode_int(message, position)
    | (let integer: ErlangInteger, let next: I32) => (integer, next)
    else _decode_big_integer(message, position, type_tag)
    end

  fun _decode_big_integer(message: EMessage ref, position: I32, type_tag: I32)
    : ((ErlangTerm | None), I32)
  =>
    let next = message.skip_term(position)
    if next < 0 then return (None, position) end

    match message.copy_bytes(position, next)
    | let raw: String =>
      try
        let large = type_tag == TermType.large_big()
        let count: USize = if large then
          (raw(1)?.usize() << 24) + (raw(2)?.usize() << 16)
            + (raw(3)?.usize() << 8) + raw(4)?.usize()
        else
          raw(1)?.usize()
        end
        let sign_at: USize = if large then 5 else 2 end
        let negative = raw(sign_at)? != 0

        let digits: Array[U8] trn = recover trn Array[U8] end
        var i: USize = 0
        while i < count do
          digits.push(raw(sign_at + 1 + i)?)
          i = i + 1
        end

        (ErlangBigInteger(negative, consume digits), next)
      else
        (None, position)
      end
    else
      (None, position)
    end

  fun _decode_float(message: EMessage ref, position: I32)
    : ((ErlangTerm | None), I32)
  =>
    (let value, let next) = message.decode_double(position)
    match value
    | let number: F64 => (ErlangFloat(number), next)
    else (None, position)
    end

  fun _decode_binary(message: EMessage ref, position: I32)
    : ((ErlangTerm | None), I32)
  =>
    (let value, let next) = message.decode_binary(position)
    match value
    | let bytes: String => (ErlangBinary(bytes), next)
    else (None, position)
    end

  fun _decode_bitstring(message: EMessage ref, position: I32)
    : ((ErlangTerm | None), I32)
  =>
    (let value, let next) = message.decode_bitstring(position)
    match value
    | let bits: ErlangBitstring => (bits, next)
    else (None, position)
    end

  fun _decode_string(message: EMessage ref, position: I32)
    : ((ErlangTerm | None), I32)
  =>
    (let value, let next) = message.decode_string(position)
    match value
    | let text: String => (ErlangString(text), next)
    else (None, position)
    end

  fun _decode_pid(message: EMessage ref, position: I32)
    : ((ErlangTerm | None), I32)
  =>
    (let value, let next) = message.decode_pid(position)
    match value
    | let pid: ErlangPid => (pid, next)
    else (None, position)
    end

  fun _decode_port(message: EMessage ref, position: I32)
    : ((ErlangTerm | None), I32)
  =>
    (let value, let next) = message.decode_port(position)
    match value
    | let port: ErlangPort => (port, next)
    else (None, position)
    end

  fun _decode_reference(message: EMessage ref, position: I32)
    : ((ErlangTerm | None), I32)
  =>
    (let value, let next) = message.decode_reference(position)
    match value
    | let reference: ErlangReference => (reference, next)
    else (None, position)
    end

  fun _decode_tuple(message: EMessage ref, position: I32)
    : ((ErlangTerm | None), I32)
  =>
    (let arity, let after_header) = message.decode_tuple_header(position)
    if not _plausible_arity(message, arity) then return (None, position) end

    let elements: Array[ErlangTerm] trn = recover trn Array[ErlangTerm] end
    var at = after_header
    var i: I32 = 0
    while i < arity do
      (let element, let next) = decode_at(message, at)
      match element
      | let term: ErlangTerm =>
        elements.push(term)
        at = next
      else
        return (None, position)
      end
      i = i + 1
    end

    (ErlangTuple(consume elements), at)

  fun _decode_list(message: EMessage ref, position: I32)
    : ((ErlangTerm | None), I32)
  =>
    (let arity, let after_header) = message.decode_list_header(position)
    if not _plausible_arity(message, arity) then return (None, position) end

    let elements: Array[ErlangTerm] trn = recover trn Array[ErlangTerm] end
    var at = after_header
    var i: I32 = 0
    while i < arity do
      (let element, let next) = decode_at(message, at)
      match element
      | let term: ErlangTerm =>
        elements.push(term)
        at = next
      else
        return (None, position)
      end
      i = i + 1
    end

    // A list is terminated by the empty list when it is proper.
    (let terminator, _) = message.type_at(at)
    if terminator == TermType.nil() then
      (ErlangList(consume elements), at + 1)
    else
      (let tail, let next) = decode_at(message, at)
      match tail
      | let term: ErlangTerm => (ErlangList(consume elements, term), next)
      else (None, position)
      end
    end

  fun _decode_map(message: EMessage ref, position: I32)
    : ((ErlangTerm | None), I32)
  =>
    (let arity, let after_header) = message.decode_map_header(position)
    if not _plausible_arity(message, arity) then return (None, position) end

    let pairs: Array[(ErlangTerm, ErlangTerm)] trn =
      recover trn Array[(ErlangTerm, ErlangTerm)] end
    var at = after_header
    var i: I32 = 0
    while i < arity do
      (let key, let after_key) = decode_at(message, at)
      match key
      | let key_term: ErlangTerm =>
        (let value, let after_value) = decode_at(message, after_key)
        match value
        | let value_term: ErlangTerm =>
          pairs.push((key_term, value_term))
          at = after_value
        else
          return (None, position)
        end
      else
        return (None, position)
      end
      i = i + 1
    end

    (ErlangMap(consume pairs), at)

  fun _decode_verbatim(message: EMessage ref, position: I32, type_tag: I32)
    : ((ErlangTerm | None), I32)
  =>
    """Keeps a term this library does not model, to be forwarded unchanged."""
    let next = message.skip_term(position)
    if next < 0 then return (None, position) end

    match message.copy_bytes(position, next)
    | let raw: String => (ErlangOpaque(TermType.name(type_tag), raw), next)
    else (None, position)
    end

  fun _plausible_arity(message: EMessage ref, arity: I32): Bool =>
    """
    An arity larger than the message length cannot be real, because every
    element takes at least one byte.  Rejecting it keeps a small message from
    reserving an enormous array.
    """
    (arity >= 0) and (arity.i64() <= message.length().i64())