Erlang term encoder

primitive ErlangTermEncoder
  """
  Writes a term into an `EMessage`.

  The functions are partial: an encoding can fail when the message cannot grow
  or when a value cannot be represented in the external format.
  """

  fun encode(message: EMessage ref, term: ErlangTerm) ? =>
    match term
    | let t: ErlangAtom => _check(message.encode_atom(t.name))?
    | let t: ErlangInteger => _check(message.encode_int(t.value))?
    | let t: ErlangBigInteger =>
      _check(message.append_raw(_big_integer_bytes(t)))?
    | let t: ErlangFloat => _check(message.encode_double(t.value))?
    | let t: ErlangBinary => _check(message.encode_binary(t.value))?
    | let t: ErlangBitstring =>
      _check(message.encode_bitstring(t.value, t.used_bits_in_last_byte))?
    | let t: ErlangString => _check(message.encode_string(t.value))?
    | let t: ErlangList => _encode_list(message, t)?
    | let t: ErlangTuple => _encode_tuple(message, t)?
    | let t: ErlangMap => _encode_map(message, t)?
    | let t: ErlangPid =>
      _check(message.encode_pid(t.node, t.num, t.serial, t.creation))?
    | let t: ErlangPort =>
      _check(message.encode_port(t.node, t.id, t.creation))?
    | let t: ErlangReference =>
      _check(message.encode_reference(t.node, t.ids, t.creation))?
    | let t: ErlangOpaque => _check(message.append_raw(t.raw))?
    end

  fun _encode_list(message: EMessage ref, list: ErlangList) ? =>
    if list.elements.size() == 0 then
      // An element-less list can only be the tail of an improper list.
      match list.tail
      | let tail: ErlangTerm => encode(message, tail)?
      else _check(message.encode_empty_list())?
      end
      return
    end

    _check(message.encode_list_header(list.elements.size()))?
    for element in list.elements.values() do
      encode(message, element)?
    end

    match list.tail
    | let tail: ErlangTerm => encode(message, tail)?
    else _check(message.encode_empty_list())?
    end

  fun _encode_tuple(message: EMessage ref, tuple: ErlangTuple) ? =>
    _check(message.encode_tuple_header(tuple.elements.size()))?
    for element in tuple.elements.values() do
      encode(message, element)?
    end

  fun _encode_map(message: EMessage ref, map: ErlangMap) ? =>
    _check(message.encode_map_header(map.pairs.size()))?
    for (key, value) in map.pairs.values() do
      encode(message, key)?
      encode(message, value)?
    end

  fun _big_integer_bytes(big: ErlangBigInteger): String val^ =>
    """
    The external format for a big integer: a length, a sign byte and the
    magnitude in base 256, least significant byte first.
    """
    let bytes: Array[U8] trn = recover trn Array[U8] end
    let count = big.digits.size()
    if count <= 255 then
      bytes.push(TermType.small_big().u8())
      bytes.push(count.u8())
    else
      bytes.push(TermType.large_big().u8())
      bytes.push((count >> 24).u8())
      bytes.push((count >> 16).u8())
      bytes.push((count >> 8).u8())
      bytes.push(count.u8())
    end

    bytes.push(if big.negative then 1 else 0 end)
    for digit in big.digits.values() do
      bytes.push(digit)
    end

    String.from_array(consume bytes)

  fun _check(result: I32) ? =>
    if result != 0 then error end