Code for this post is available at ed4ee0b78.

Erie now has a very basic type checker. It supports simple builtin types like Integer and String, and the slightly more complex polymorphic list and tuple types. User-defined types are not supported yet.

Testing

The type checker has provided a pretty amazing opportunity for test-driven development. The type checker code has so many paths and branches that having a complete test suite helps prevent me from introducing regression bugs.

I have also found assert_raise to be indespensible. A proper type checker would collect errors and print them all at the end of compilation, but for now, it raises errors when the type checker finds a mismatch.

test "literal list type fails with mismatched types" do
  code = """
  (sig one [] [Integer])
  (def one []
    [1 2 "3" 4])
  """

  {:ok, forms} = Parser.parse(code)
  translator = Translator.from_parsed(forms, {:Core, 1}, false)

  assert_raise RuntimeError, fn ->
    TypeChecker.check(translator)
  end
end

Error Messages

When writing a type checker, one should aspire to the level of detail and helpfulness in Elm type checker messages. The errors raised by the Erie type checker right now are just good enough to help me debug any example Erie code I write, but definitely not good enough to live in the compiler long-term.

def expression_type({:var, _, name}, bindings, _signatures) do
  bindings
  |> Enum.find(fn {n, _} -> n == name end)
  |> case do
    nil -> raise "Returning a binding `#{name}` that isn't a function parameter"
    {_, type} -> type
  end
end

As much as I’d like to add great error messages now, that’s a major project in and of itself.