In the previous article, we converted not quite Erie code into Erlang Abstract Format that could be compiled, loaded, and run. I say “not quite” because the code was intentially simplified to get the lexer and parser working. Now we’re going to parse real Erie code with arguments and types in the function definitions. For now all of the type information will be ignored to get things running.

We’ll start with a 0-arity function. The empty list of arguments is represented with [], the return type with the symbol Integer, and the last element is the hardcoded return value of 1.

I’ve been using this guide from Viktor Söderqvist to help me write the tests’ expected values in Erlang Abstract Format.

test "0 arity" do
  code = """
  (defmodule Core)

  (def give_me_one [] Integer 1)
  """

  {:ok, forms} = Parser.parse(code)

  assert {:ok,
          [
            {:attribute, 1, :module, :"Erie.Core"},
            {:attribute, 1, :export, [give_me_one: 0]},
            {:function, 3, :give_me_one, 0,
              [
                {:clause, 3, [], [], [{:integer, 3, 1}]}
              ]}
          ]} == Translator.to_eaf(forms)
end

The first thing we need to do is read the name of the module. We can be naïve and expect this to always be the first line. The output from our parser will look something like this. Note the new :symbol category that didn’t exist in the previous article.

[
  [{:atom, 1, :defmodule}, {:symbol, 1, :Core}],
  [
    {:atom, 3, :def},
    {:atom, 3, :give_me_one},
    [],
    {:symbol, 3, :Integer},
    {:atom, 3, 1}
  ]
]

The new :symbol category is applied to any atom that begins with ' or a capital letter i.e. 'my_symbol or MySymbol. To translate this code to EAF, we’ll build a specialized function that matches the entire defmodule list. We automatically prepend Erie. to the module name, similar to what Elixir does, to provide some namespacing.

def extract_module([[{:atom, _, :defmodule}, {:symbol, line, mod}] | rest]) do
  mod = ("Erie." <> Atom.to_string(mod)) |> String.to_atom()
  {:ok, mod, line, rest}
end

Once we have the module, we can translate each function. Here we’re matching on the def atom, the name of the function, an empty argument list, and ignoring the return type. We’re also storing the function name and arity in a seperate structure so we can export it later to make it publicy invokable.

def translate(struct, [form | tail], ret) do
  case form do
    [{:atom, _, :def}, {:atom, line, name}, [], {:symbol, _l2, _ret_type} | body] ->
      body = translate_body(body)

      %{struct | functions: [{name, 0} | struct.functions]}
      |> translate(tail, [
        {:function, line, name, arity, [{:clause, line, [], [], body}]}
        | ret
      ])
  end
end

def translate_body([{:integer, line, val} | rest], accum) do
  translate_body(rest, [{:integer, line, val} | accum])
end

def translate_body([], accum) do
  accum
en

This is able to successfully translate the 0-arity function, which is great, but obviously not enough. Next we’ll translate a 1-arity function that returns the same integer that was passed as an argument.

test "1 arity" do
  code = """
  (defmodule Core)

  (def identity ['x Integer] Integer x)
  """

  {:ok, forms} = Parser.parse(code)

  assert {:ok,
          [
            {:attribute, 1, :module, :"Erie.Core"},
            {:attribute, 1, :export, [identity: 1]},
            {:function, 3, :identity, 1,
              [
                {:clause, 3, [{:var, 3, :x}], [], [{:var, 3, :x}]}
              ]}
          ]} == Translator.to_eaf(forms)
end

To handle the identity function, we need to translate the arguments and the body. Popping the first element(s) off their respective list, converting to EAF, then rebuilding the list has a been a very simple and straightforward solution.

def translate(struct, [form | tail], ret) do
  case form do
    [{:atom, _, :def}, {:atom, line, name}, params, {:symbol, _l2, _return_type} | body] ->
      body = translate_body(body)
      params = translate_params(params)
      arity = Enum.count(params)

      %{struct | functions: [{name, arity} | struct.functions]}
      |> translate(tail, [
        {:function, line, name, arity, [{:clause, line, params, [], body}]}
        | ret
      ])
  end
end

def translate_body(list) do
  list |> translate_body([]) |> Enum.reverse()
end

def translate_body([{:atom, line, val} | rest], accum) do
  translate_body(rest, [{:var, line, val} | accum])
end

def translate_body([], accum) do
  accum
end

def translate_params(list) do
  list |> translate_params([]) |> Enum.reverse()
end

def translate_params([{:symbol, line, name}, {:symbol, _, _type} | rest], accum) do
  translate_params(rest, [{:var, line, name} | accum])
end

def translate_params([], accum) do
  accum
end

With a couple of tweaks to our .yrl and .xrl files and this code, we can parse something that actually looks like Erie. It also puts us in a good place to extend the parsing code and handle more advanced scenarios.