Erlang term printer

primitive ErlangTermPrinter
  """
  Renders a term in Erlang syntax, for logs, error messages and tests.

  This is a reading aid, not a serialiser: use an `EMessage` to put a term on
  the wire.
  """

  fun string(term: ErlangTerm): String iso^ =>
    recover
      let text = String
      _write(text, term)
      text
    end

  fun _write(text: String ref, term: ErlangTerm) =>
    match term
    | let t: ErlangAtom => _write_atom(text, t.name)
    | let t: ErlangInteger =>
      text.append(t.value.string())
    | let t: ErlangBigInteger =>
      text.append(t.decimal())
    | let t: ErlangFloat => _write_float(text, t.value)
    | let t: ErlangBinary =>
      text.append("<<")
      _write_quoted(text, t.value)
      text.append(">>")
    | let t: ErlangBitstring => _write_bitstring(text, t)
    | let t: ErlangString => _write_quoted(text, t.value)
    | let t: ErlangList => _write_list(text, t)
    | let t: ErlangTuple => _write_elements(text, "{", t.elements, "}")
    | let t: ErlangMap => _write_map(text, t)
    | let t: ErlangPid => text.append(t.string())
    | let t: ErlangPort =>
      text.append("#Port<")
      text.append(t.id.string())
      text.push('.')
      text.append(t.creation.string())
      text.push('>')
    | let t: ErlangReference => _write_reference(text, t)
    | let t: ErlangOpaque =>
      text.append("#<")
      text.append(t.type_name)
      text.append(", ")
      text.append(t.raw.size().string())
      text.append(" bytes>")
    end

  fun _write_atom(text: String ref, name: String) =>
    if _is_bare_atom(name) then
      text.append(name)
    else
      text.push('\'')
      for byte in name.values() do
        if (byte == '\'') or (byte == '\\') then text.push('\\') end
        text.push(byte)
      end
      text.push('\'')
    end

  fun _write_float(text: String ref, value: F64) =>
    let printed: String ref = value.string()
    text.append(printed)

    // Erlang needs a decimal point to read a literal as a float.
    var plain = printed.size() > 0
    for byte in printed.values() do
      if not (((byte >= '0') and (byte <= '9')) or (byte == '-')) then
        plain = false
      end
    end
    if plain then text.append(".0") end

  fun _write_bitstring(text: String ref, bitstring: ErlangBitstring) =>
    text.append("<<")
    if bitstring.used_bits_in_last_byte == 8 then
      _write_quoted(text, bitstring.value)
    else
      var index: USize = 0
      for byte in bitstring.value.values() do
        if index > 0 then text.append(", ") end
        text.append(byte.string())
        index = index + 1
      end
      if index > 0 then
        text.push(':')
        text.append(bitstring.used_bits_in_last_byte.string())
      end
    end
    text.append(">>")

  fun _write_list(text: String ref, list: ErlangList) =>
    text.push('[')
    _write_terms(text, list.elements)
    match list.tail
    | let tail: ErlangTerm =>
      text.append(" | ")
      _write(text, tail)
    end
    text.push(']')

  fun _write_map(text: String ref, map: ErlangMap) =>
    text.append("#{")
    var index: USize = 0
    for (key, value) in map.pairs.values() do
      if index > 0 then text.append(", ") end
      _write(text, key)
      text.append(" => ")
      _write(text, value)
      index = index + 1
    end
    text.push('}')

  fun _write_reference(text: String ref, reference: ErlangReference) =>
    text.append("#Ref<")
    if reference.node != "" then
      text.append(reference.node)
      text.push('.')
    end
    var index: USize = 0
    for word in reference.ids.values() do
      if index > 0 then text.push('.') end
      text.append(word.string())
      index = index + 1
    end
    if index > 0 then text.push('.') end
    text.append(reference.creation.string())
    text.push('>')

  fun _write_elements(text: String ref, opening: String,
    elements: Array[ErlangTerm] val, closing: String)
  =>
    text.append(opening)
    _write_terms(text, elements)
    text.append(closing)

  fun _write_terms(text: String ref, elements: Array[ErlangTerm] val) =>
    var index: USize = 0
    for element in elements.values() do
      if index > 0 then text.append(", ") end
      _write(text, element)
      index = index + 1
    end

  fun _write_quoted(text: String ref, value: String) =>
    text.push('"')
    for byte in value.values() do
      match byte
      | '"' => text.append("\\\"")
      | '\\' => text.append("\\\\")
      | '\n' => text.append("\\n")
      | '\r' => text.append("\\r")
      | '\t' => text.append("\\t")
      else
        if (byte >= 32) and (byte < 127) then
          text.push(byte)
        else
          text.append("\\x")
          text.push(_hex_digit(byte >> 4))
          text.push(_hex_digit(byte and 0x0f))
        end
      end
    end
    text.push('"')

  fun _hex_digit(value: U8): U8 =>
    if value < 10 then '0' + value else 'A' + (value - 10) end

  fun _is_bare_atom(name: String): Bool =>
    var first = true
    for byte in name.values() do
      if first then
        if (byte < 'a') or (byte > 'z') then return false end
        first = false
      elseif not _is_atom_char(byte) then
        return false
      end
    end
    (not first) and (not _is_reserved(name))

  fun _is_atom_char(byte: U8): Bool =>
    ((byte >= 'a') and (byte <= 'z'))
      or ((byte >= 'A') and (byte <= 'Z'))
      or ((byte >= '0') and (byte <= '9'))
      or (byte == '_')
      or (byte == '@')

  fun _is_reserved(name: String): Bool =>
    match name
    | "after" | "and" | "andalso" | "band" | "begin" | "bnot" | "bor"
    | "bsl" | "bsr" | "bxor" | "case" | "catch" | "cond" | "div" | "end"
    | "fun" | "if" | "let" | "not" | "of" | "or" | "orelse" | "receive"
    | "rem" | "try" | "when" | "xor" => true
    else false
    end