Repository: fika-lang/fika
Pull request #40 (2020-10-27 00:53:18)

feat: add structs for some basic types
@emilsoman I've mostly done the basic structure for the higher-order data structures for types. However, I've hit this wall regarding the actual written typespecs. For instance: 
```
def foo(a: Int) : Int do
1
end
```

Is parsed as having return type "Int", but I need it to be `:Int`. Do you think that it's in the scope of this PR to make this conversion? If not, I have an idea on how to work around this. Same for the arg types

polvalente: @emilsoman I'm not really sure if the last commits, which define records and tuples in the parser, are needed, since there was no gain from doing this other than better naming for things.

Patch:
diff --git a/lib/fika/env.ex b/lib/fika/env.ex
index bffed92..74499e7 100644
--- a/lib/fika/env.ex
+++ b/lib/fika/env.ex
@@ -10,32 +10,32 @@ defmodule Fika.Env do
       # This is a map of known function signatures and their return types.
       # As a hack for now, we store kernel function signatures right here.
       function_types: %{
-        "kernel.+(Int,Int)" => "Int",
-        "kernel.+(Int,Float)" => "Float",
-        "kernel.+(Float,Int)" => "Float",
-        "kernel.+(Float,Float)" => "Float",
-        "kernel.-(Int,Int)" => "Int",
-        "kernel.-(Int,Float)" => "Float",
-        "kernel.-(Float,Int)" => "Float",
-        "kernel.-(Float,Float)" => "Float",
-        "kernel.*(Int,Int)" => "Int",
-        "kernel.*(Int,Float)" => "Float",
-        "kernel.*(Float,Int)" => "Float",
-        "kernel.*(Float,Float)" => "Float",
-        "kernel./(Int,Int)" => "Float",
-        "kernel./(Int,Float)" => "Float",
-        "kernel./(Float,Int)" => "Float",
-        "kernel./(Float,Float)" => "Float",
-        "kernel.|(Bool,Bool)" => "Bool",
-        "kernel.&(Bool,Bool)" => "Bool",
-        "kernel.!(Bool)" => "Bool",
-        "kernel.-(Int)" => "Int",
-        "kernel.<(Int,Int)" => "Bool",
-        "kernel.>(Int,Int)" => "Bool",
-        "kernel.<=(Int,Int)" => "Bool",
-        "kernel.>=(Int,Int)" => "Bool",
-        "kernel.==(Int,Int)" => "Bool",
-        "kernel.!=(Int,Int)" => "Bool"
+        "kernel.+(Int, Int)" => :Int,
+        "kernel.+(Int, Float)" => :Float,
+        "kernel.+(Float, Int)" => :Float,
+        "kernel.+(Float, Float)" => :Float,
+        "kernel.-(Int, Int)" => :Int,
+        "kernel.-(Int, Float)" => :Float,
+        "kernel.-(Float, Int)" => :Float,
+        "kernel.-(Float, Float)" => :Float,
+        "kernel.*(Int, Int)" => :Int,
+        "kernel.*(Int, Float)" => :Float,
+        "kernel.*(Float, Int)" => :Float,
+        "kernel.*(Float, Float)" => :Float,
+        "kernel./(Int, Int)" => :Float,
+        "kernel./(Int, Float)" => :Float,
+        "kernel./(Float, Int)" => :Float,
+        "kernel./(Float, Float)" => :Float,
+        "kernel.|(Bool, Bool)" => :Bool,
+        "kernel.&(Bool, Bool)" => :Bool,
+        "kernel.!(Bool)" => :Bool,
+        "kernel.-(Int)" => :Int,
+        "kernel.<(Int, Int)" => :Bool,
+        "kernel.>(Int, Int)" => :Bool,
+        "kernel.<=(Int, Int)" => :Bool,
+        "kernel.>=(Int, Int)" => :Bool,
+        "kernel.==(Int, Int)" => :Bool,
+        "kernel.!=(Int, Int)" => :Bool
       }
     }
   end
diff --git a/lib/fika/parser/helper.ex b/lib/fika/parser/helper.ex
index 25a70ff..cb88002 100644
--- a/lib/fika/parser/helper.ex
+++ b/lib/fika/parser/helper.ex
@@ -1,6 +1,8 @@
 defmodule Fika.Parser.Helper do
   import NimbleParsec
 
+  alias Fika.Types, as: T
+
   def to_ast(c, kind) do
     c
     |> line()
@@ -50,27 +52,37 @@ defmodule Fika.Parser.Helper do
   end
 
   def do_to_ast({[], line}, :return_type) do
-    {:type, line, "Nothing"}
+    {:type, line, :Nothing}
   end
 
   def do_to_ast({[type], _line}, :return_type) do
     type
   end
 
-  def do_to_ast({[name], _line}, :simple_type) do
-    name
+  def do_to_ast({[{_, _, inner_type}], _line}, :list_type) when is_struct(inner_type) do
+    %T.List{type: inner_type}
   end
 
-  def do_to_ast({types, line}, :type) do
-    type =
-      Enum.reduce(types, "", fn
-        {:atom, _l, value}, acc ->
-          acc <> ":#{value}"
+  def do_to_ast({[inner_type], _line}, :list_type) do
+    %T.List{type: inner_type}
+  end
 
-        type, acc ->
-          acc <> type
-      end)
+  def do_to_ast({[{:atom, _, atom}], _line}, :atom_type) do
+    atom
+  end
+
+  def do_to_ast({types, _line}, :function_type) do
+    arg_types = types |> Keyword.take([:arg_type]) |> Keyword.values()
+    return_type = Keyword.get(types, :return_type)
+
+    %T.FunctionRef{arg_types: arg_types, return_type: return_type}
+  end
 
+  def do_to_ast({[{_, _, type}], line}, :type) when is_struct(type) do
+    {:type, line, type}
+  end
+
+  def do_to_ast({[type], line}, :type) do
     {:type, line, type}
   end
 
@@ -163,4 +175,10 @@ defmodule Fika.Parser.Helper do
   defp value_from_identifier({:identifier, _line, value}) do
     value
   end
+
+  def to_atom([value]), do: String.to_atom(value)
+
+  def tag(value, tag) do
+    {tag, value}
+  end
 end
diff --git a/lib/fika/parser/types.ex b/lib/fika/parser/types.ex
index 7ffcfae..2efca55 100644
--- a/lib/fika/parser/types.ex
+++ b/lib/fika/parser/types.ex
@@ -7,38 +7,29 @@ defmodule Fika.Parser.Types do
   identifier_str = parsec({Common, :identifier_str})
   atom = parsec({Common, :atom})
 
+  atom_type = Helper.to_ast(atom, :atom_type)
+
   type_args =
-    optional(
+    parsec(:type)
+    |> optional(
       allow_space
-      |> string(",")
+      |> ignore(string(","))
       |> concat(allow_space)
-      |> parsec(:type)
       |> parsec(:type_args)
     )
 
   function_type =
-    string("Fn")
-    |> string("(")
-    |> optional(parsec(:type) |> concat(type_args))
+    ignore(string("Fn("))
+    |> optional(type_args |> map({Helper, :tag, [:arg_type]}))
     |> concat(allow_space)
-    |> string("->")
+    |> ignore(string("->"))
     |> concat(allow_space)
-    |> parsec(:type)
-    |> string(")")
-
-  simple_type =
-    ascii_string([?A..?Z], 1)
-    |> ascii_string([?a..?z, ?A..?Z], min: 0)
-    |> reduce({Enum, :join, [""]})
-    |> Helper.to_ast(:simple_type)
-
-  type_parens =
-    string("(")
-    |> concat(allow_space)
-    |> parsec(:type)
-    |> concat(type_args)
-    |> concat(allow_space)
-    |> string(")")
+    |> concat(
+      parsec(:type)
+      |> map({Helper, :tag, [:return_type]})
+    )
+    |> ignore(string(")"))
+    |> Helper.to_ast(:function_type)
 
   type_key_value =
     allow_space
@@ -80,34 +71,49 @@ defmodule Fika.Parser.Types do
     |> string(")")
     |> label("map type")
 
-  # To parse functions with tuple return type
-  type_tuple_element =
-    parsec(:type)
-    |> label("tuple element")
-
-  type_tuple_elements =
-    type_tuple_element
-    |> repeat(
-      allow_space
-      |> ignore(string(","))
-      |> concat(allow_space)
-      |> concat(type_tuple_element)
-    )
-    |> reduce({Enum, :join, [","]})
-
   tuple_type =
     string("{")
-    |> concat(type_tuple_elements)
+    |> concat(type_args)
     |> string("}")
     |> reduce({Enum, :join, []})
     |> label("tuple type")
 
+  list_type =
+    ignore(string("List("))
+    |> concat(parsec(:type))
+    |> ignore(string(")"))
+    |> label("list type")
+    |> Helper.to_ast(:list_type)
+
+  string_type =
+    string("String")
+    |> label("string")
+    |> reduce({Helper, :to_atom, []})
+
+  int_type =
+    string("Int")
+    |> label("int")
+    |> reduce({Helper, :to_atom, []})
+
+  float_type =
+    string("Float")
+    |> label("float")
+    |> reduce({Helper, :to_atom, []})
+
+  nothing_type =
+    string("Nothing")
+    |> label("nothing")
+    |> reduce({Helper, :to_atom, []})
+
   type =
     choice([
+      string_type,
+      int_type,
+      float_type,
+      nothing_type,
+      atom_type,
       function_type,
-      simple_type
-      |> optional(type_parens),
-      atom,
+      list_type,
       record_type,
       map_type,
       tuple_type
diff --git a/lib/fika/type_checker.ex b/lib/fika/type_checker.ex
index 016abcb..6814eea 100644
--- a/lib/fika/type_checker.ex
+++ b/lib/fika/type_checker.ex
@@ -1,6 +1,6 @@
 defmodule Fika.TypeChecker do
   alias Fika.Env
-  alias Fika.Types.FunctionRef
+  alias Fika.Types, as: T
   require Logger
 
   # Given the AST of a module, this function type checks each of the
@@ -32,8 +32,7 @@ defmodule Fika.TypeChecker do
   # Given the AST of a function definition, this function checks if the return
   # type is indeed the type that's inferred from the body of the function.
   def check({:function, _line, {_, _, return_type, _}} = function, env) do
-    {:type, _line, type} = return_type
-    expected_type = type
+    {:type, _line, expected_type} = return_type
 
     case infer(function, env) do
       {:ok, ^expected_type, _env} = result ->
@@ -57,12 +56,11 @@ defmodule Fika.TypeChecker do
     env
     |> infer_block(exprs)
     |> add_function_type(name, args)
-    |> to_str()
   end
 
   def infer_block(env, []) do
     Logger.debug("Block is empty.")
-    {:ok, "Nothing", env}
+    {:ok, :Nothing, env}
   end
 
   def infer_block(env, [exp]) do
@@ -82,13 +80,13 @@ defmodule Fika.TypeChecker do
   # Integer literals
   def infer_exp(env, {:integer, _line, integer}) do
     Logger.debug("Integer #{integer} found. Type: Int")
-    {:ok, "Int", env}
+    {:ok, :Int, env}
   end
 
   # Booleans
   def infer_exp(env, {:boolean, _line, boolean}) do
     Logger.debug("Boolean #{boolean} found. Type: Bool")
-    {:ok, "Bool", env}
+    {:ok, :Bool, env}
   end
 
   # Variables
@@ -116,7 +114,7 @@ defmodule Fika.TypeChecker do
   # exp has to be a function ref type
   def infer_exp(env, {:call, {exp, _line}, args}) do
     case infer_exp(env, exp) do
-      {:ok, %FunctionRef{arg_types: arg_types, return_type: type}, env} ->
+      {:ok, %T.FunctionRef{arg_types: arg_types, return_type: type}, env} ->
         case do_infer_args_without_name(env, args) do
           {:ok, ^arg_types, env} ->
             {:ok, type, env}
@@ -124,8 +122,8 @@ defmodule Fika.TypeChecker do
           {:ok, other_arg_types, _env} ->
             error =
               "Expected function reference to be called with" <>
-                " arguments (#{Enum.join(arg_types, ",")}), but it was called " <>
-                "with arguments (#{Enum.join(other_arg_types, ",")})"
+                " arguments (#{T.Helper.join_list(arg_types)}), but it was called " <>
+                "with arguments (#{T.Helper.join_list(other_arg_types)})"
 
             {:error, error}
         end
@@ -156,13 +154,13 @@ defmodule Fika.TypeChecker do
     Enum.reduce_while(string_parts, env, fn
       string, env when is_binary(string) ->
         Logger.debug("String #{string} found. Type: String")
-        {:cont, {:ok, "String", env}}
+        {:cont, {:ok, :String, env}}
 
       exp, env ->
         Logger.debug("String interpolation found. Inferring type of expression")
 
         case infer_exp(env, exp) do
-          {:ok, "String", _env} = result ->
+          {:ok, :String, _env} = result ->
             {:cont, result}
 
           {:ok, other_type, _env} ->
@@ -186,12 +184,7 @@ defmodule Fika.TypeChecker do
   def infer_exp(env, {:tuple, _, exps}) do
     case do_infer_tuple_exps(exps, env) do
       {:ok, exp_types, env} ->
-        exp_types =
-          exp_types
-          |> Enum.reverse()
-          |> Enum.join(",")
-
-        {:ok, "{#{exp_types}}", env}
+        {:ok, %T.Tuple{elements: exp_types}, env}
 
       error ->
         error
@@ -206,12 +199,7 @@ defmodule Fika.TypeChecker do
     else
       case do_infer_key_values(key_values, env) do
         {:ok, k_v_types, env} ->
-          k_v_types =
-            k_v_types
-            |> Enum.reverse()
-            |> Enum.join(",")
-
-          {:ok, "{#{k_v_types}}", env}
+          {:ok, %T.Record{fields: k_v_types}, env}
 
         error ->
           error
@@ -225,28 +213,23 @@ defmodule Fika.TypeChecker do
     {key, value} = kv
 
     with {:ok, key_type, env} <- infer_exp(env, key),
-         {:ok, val_type, env} <- infer_exp(env, value) do
-      final_type = "Map(#{key_type},#{val_type})"
+         {:ok, value_type, env} <- infer_exp(env, value) do
+      map_type = %T.Map{key_type: key_type, value_type: value_type}
 
-      Enum.reduce_while(rest_kvs, {:ok, final_type, env}, fn {k, v}, {:ok, acc_type, acc_env} ->
-        case infer_exp(acc_env, k) do
-          {:ok, ^key_type, env} ->
-            case infer_exp(env, v) do
-              {:ok, ^val_type, env} ->
-                acc = {:ok, acc_type, env}
-                {:cont, acc}
+      Enum.reduce_while(rest_kvs, {:ok, map_type, env}, fn {k, v}, {:ok, type, env} ->
+        %{key_type: key_type, value_type: value_type} = type
 
-              {:ok, diff_type, _} ->
-                error = {:error, "Expected map value of type #{val_type}, but got #{diff_type}"}
-
-                {:halt, error}
+        with {:key, {:ok, ^key_type, env}} <- {:key, infer_exp(env, k)},
+             {:value, {:ok, ^value_type, env}} <- {:value, infer_exp(env, v)} do
+          {:cont, {:ok, type, env}}
+        else
+          {:key, {:ok, diff_type, _}} ->
+            error = {:error, "Expected map key of type #{key_type}, but got #{diff_type}"}
+            {:halt, error}
 
-              error ->
-                {:halt, error}
-            end
+          {:value, {:ok, diff_type, _}} ->
+            error = {:error, "Expected map value of type #{value_type}, but got #{diff_type}"}
 
-          {:ok, diff_type, _} ->
-            error = {:error, "Expected map key of type #{key_type}, but got #{diff_type}"}
             {:halt, error}
 
           error ->
@@ -257,10 +240,14 @@ defmodule Fika.TypeChecker do
   end
 
   # Function ref
-  def infer_exp(env, {:function_ref, _, {module, function_name, arg_types}}) do
+  def infer_exp(
+        env,
+        {:function_ref, _, {module, function_name, arg_types}}
+      ) do
     Logger.debug("Inferring type of function: #{function_name}")
 
     module_name = module || Env.current_module(env)
+
     signature = get_signature(module_name, function_name, arg_types)
 
     result =
@@ -276,8 +263,8 @@ defmodule Fika.TypeChecker do
       end
 
     case result do
-      {:ok, type, env} ->
-        type = %FunctionRef{arg_types: arg_types, return_type: type}
+      {:ok, return_type, env} ->
+        type = %T.FunctionRef{return_type: return_type, arg_types: arg_types}
         {:ok, type, env}
 
       error ->
@@ -288,7 +275,7 @@ defmodule Fika.TypeChecker do
   # Atom value
   def infer_exp(env, {:atom, _line, atom}) do
     Logger.debug("Atom value found. Type: #{atom}")
-    {:ok, ":#{atom}", env}
+    {:ok, atom, env}
   end
 
   # if-else expression
@@ -296,16 +283,16 @@ defmodule Fika.TypeChecker do
     Logger.debug("Inferring an if-else expression")
 
     case infer_if_else_condition(env, condition) do
-      {:ok, "Bool", env} -> infer_if_else_blocks(env, if_block, else_block)
+      {:ok, :Bool, env} -> infer_if_else_blocks(env, if_block, else_block)
       error -> error
     end
   end
 
   defp infer_if_else_condition(env, condition) do
     case infer_exp(env, condition) do
-      {:ok, "Bool", env} ->
+      {:ok, :Bool, env} ->
         Logger.debug("if-else condition has return type: Bool")
-        {:ok, "Bool", env}
+        {:ok, :Bool, env}
 
       {_, inferred_type, _env} ->
         Logger.debug("if-else condition has wrong return type: #{inferred_type}")
@@ -335,7 +322,7 @@ defmodule Fika.TypeChecker do
       case infer_exp(env, v) do
         {:ok, type, env} ->
           {:identifier, _, key} = k
-          {:cont, {:ok, ["#{key}:#{type}" | acc], env}}
+          {:cont, {:ok, [{key, type} | acc], env}}
 
         error ->
           {:halt, error}
@@ -353,6 +340,13 @@ defmodule Fika.TypeChecker do
           {:halt, error}
       end
     end)
+    |> case do
+      {:ok, reversed_exp_types, env} ->
+        {:ok, Enum.reverse(reversed_exp_types), env}
+
+      error ->
+        error
+    end
   end
 
   def infer_args(env, exp, module) do
@@ -377,12 +371,12 @@ defmodule Fika.TypeChecker do
   end
 
   defp infer_list_exps(env, []) do
-    {:ok, "List(Nothing)", env}
+    {:ok, %T.List{}, env}
   end
 
   defp infer_list_exps(env, [exp]) do
     case infer_exp(env, exp) do
-      {:ok, type, env} -> {:ok, "List(#{type})", env}
+      {:ok, type, env} -> {:ok, %T.List{type: type}, env}
       error -> error
     end
   end
@@ -390,7 +384,7 @@ defmodule Fika.TypeChecker do
   defp infer_list_exps(env, [exp | rest]) do
     {:ok, type, env} = infer_exp(env, exp)
 
-    Enum.reduce_while(rest, {:ok, "List(#{type})", env}, fn exp, {:ok, acc_type, acc_env} ->
+    Enum.reduce_while(rest, {:ok, %T.List{type: type}, env}, fn exp, {:ok, acc_type, acc_env} ->
       case infer_exp(acc_env, exp) do
         {:ok, ^type, env} ->
           acc = {:ok, acc_type, env}
@@ -424,17 +418,24 @@ defmodule Fika.TypeChecker do
   end
 
   defp do_infer_args_without_name(env, args) do
-    Enum.reduce_while(args, {:ok, [], env}, fn arg, {:ok, type_acc, env} ->
+    Enum.reduce_while(args, {:ok, [], env}, fn arg, {:ok, acc, env} ->
       case infer_exp(env, arg) do
         {:ok, type, env} ->
           Logger.debug("Argument is type: #{type}")
-          {:cont, {:ok, type_acc ++ [type], env}}
+          {:cont, {:ok, [type | acc], env}}
 
         error ->
           Logger.debug("Argument cannot be inferred")
           {:halt, error}
       end
     end)
+    |> case do
+      {:ok, reversed_types, env} ->
+        {:ok, Enum.reverse(reversed_types), env}
+
+      error ->
+        error
+    end
   end
 
   defp get_type_by_signature(env, signature) do
@@ -480,7 +481,8 @@ defmodule Fika.TypeChecker do
   end
 
   defp get_signature(module, name, arg_types) do
-    "#{module}.#{name}(#{Enum.join(arg_types, ",")})"
+    arg_types_str = T.Helper.join_list(arg_types)
+    "#{module}.#{name}(#{arg_types_str})"
   end
 
   defp check_by_signature(env, signature) do
@@ -498,14 +500,4 @@ defmodule Fika.TypeChecker do
       {:error, "Undefined function: #{signature} in module #{module}"}
     end
   end
-
-  defp to_str(result) do
-    case result do
-      {:ok, type, env} ->
-        {:ok, to_string(type), env}
-
-      error ->
-        error
-    end
-  end
 end
diff --git a/lib/fika/types.ex b/lib/fika/types.ex
deleted file mode 100644
index 2173e64..0000000
--- a/lib/fika/types.ex
+++ /dev/null
@@ -1,16 +0,0 @@
-defmodule Fika.Types do
-  @moduledoc """
-  This module contains the types in Fika which are implemented using structs
-  """
-
-  defmodule FunctionRef do
-    defstruct [:arg_types, :return_type]
-
-    defimpl String.Chars, for: FunctionRef do
-      def to_string(%{arg_types: arg_types, return_type: return_type}) do
-        args = Enum.join(arg_types, ",")
-        "Fn(#{args}->#{return_type})"
-      end
-    end
-  end
-end
diff --git a/lib/fika/types/function_ref.ex b/lib/fika/types/function_ref.ex
new file mode 100644
index 0000000..3bf4b6d
--- /dev/null
+++ b/lib/fika/types/function_ref.ex
@@ -0,0 +1,12 @@
+defmodule Fika.Types.FunctionRef do
+  defstruct [:return_type, arg_types: []]
+
+  alias Fika.Types, as: T
+
+  defimpl String.Chars, for: T.FunctionRef do
+    def to_string(%{arg_types: arg_types, return_type: return_type}) do
+      arg_types_str = T.Helper.join_list(arg_types)
+      "Fn(#{arg_types_str}->#{return_type})"
+    end
+  end
+end
diff --git a/lib/fika/types/helper.ex b/lib/fika/types/helper.ex
new file mode 100644
index 0000000..5ca0358
--- /dev/null
+++ b/lib/fika/types/helper.ex
@@ -0,0 +1,9 @@
+defmodule Fika.Types.Helper do
+  @moduledoc """
+  Helper functions for dealing with types
+  """
+
+  def join_list(arg_list) when is_list(arg_list) do
+    Enum.join(arg_list, ", ")
+  end
+end
diff --git a/lib/fika/types/list.ex b/lib/fika/types/list.ex
new file mode 100644
index 0000000..7e1a1f8
--- /dev/null
+++ b/lib/fika/types/list.ex
@@ -0,0 +1,9 @@
+defmodule Fika.Types.List do
+  defstruct type: :Nothing
+
+  defimpl String.Chars, for: Fika.Types.List do
+    def to_string(%{type: type}) do
+      "List(#{type})"
+    end
+  end
+end
diff --git a/lib/fika/types/map.ex b/lib/fika/types/map.ex
new file mode 100644
index 0000000..b2ca532
--- /dev/null
+++ b/lib/fika/types/map.ex
@@ -0,0 +1,11 @@
+defmodule Fika.Types.Map do
+  defstruct [:key_type, :value_type]
+
+  alias Fika.Types, as: T
+
+  defimpl String.Chars, for: T.Map do
+    def to_string(%{key_type: key_type, value_type: value_type}) do
+      "Map(#{key_type}, #{value_type})"
+    end
+  end
+end
diff --git a/lib/fika/types/record.ex b/lib/fika/types/record.ex
new file mode 100644
index 0000000..919b5a4
--- /dev/null
+++ b/lib/fika/types/record.ex
@@ -0,0 +1,18 @@
+defmodule Fika.Types.Record do
+  defstruct [:fields]
+
+  alias Fika.Types, as: T
+
+  defimpl String.Chars, for: T.Record do
+    def to_string(%{fields: fields}) do
+      str =
+        fields
+        |> Enum.map(fn {k, v} ->
+          "#{k}: #{v}"
+        end)
+        |> T.Helper.join_list()
+
+      "{#{str}}"
+    end
+  end
+end
diff --git a/lib/fika/types/tuple.ex b/lib/fika/types/tuple.ex
new file mode 100644
index 0000000..3e1b81d
--- /dev/null
+++ b/lib/fika/types/tuple.ex
@@ -0,0 +1,12 @@
+defmodule Fika.Types.Tuple do
+  defstruct elements: []
+
+  alias Fika.Types, as: T
+
+  defimpl String.Chars, for: T.Tuple do
+    def to_string(%{elements: elements}) do
+      elements_str = T.Helper.join_list(elements)
+      "{#{elements_str}}"
+    end
+  end
+end


Test patch:
diff --git a/test/fika/parser_test.exs b/test/fika/parser_test.exs
index 740aec6..d7a69b8 100644
--- a/test/fika/parser_test.exs
+++ b/test/fika/parser_test.exs
@@ -3,6 +3,8 @@ defmodule Fika.ParserTest do
   alias TestEvaluator, as: TE
   alias TestParser, as: TP
 
+  alias Fika.Types, as: T
+
   test "integer" do
     str = """
     123
@@ -75,26 +77,26 @@ defmodule Fika.ParserTest do
     end
 
     test "arithmetic binary * and / have less precedence than unary -" do
-      assert {:ok, {-2, "Int", _}} = TE.eval("1 * -2")
-      assert {:ok, {2.0, "Float", _}} = TE.eval("-10 / -5")
+      assert {:ok, {-2, :Int, _}} = TE.eval("1 * -2")
+      assert {:ok, {2.0, :Float, _}} = TE.eval("-10 / -5")
     end
 
     test "arithmetic binary + and - have less precedence than * and /" do
-      assert {:ok, {12.5, "Float", _}} = TE.eval("1 / 2 + 3 * 4")
-      assert {:ok, {3.0, "Float", _}} = TE.eval("1 * 5 - 4 / 2")
+      assert {:ok, {12.5, :Float, _}} = TE.eval("1 / 2 + 3 * 4")
+      assert {:ok, {3.0, :Float, _}} = TE.eval("1 * 5 - 4 / 2")
     end
 
     test "comparison operators <, >, <= and >= have less precedence than arithmetic + and -" do
-      assert {:ok, {false, "Bool", _}} = TE.eval("1 + 2 < 4 - 1")
-      assert {:ok, {true, "Bool", _}} = TE.eval("1 + 2 <= 4 - 1")
-      assert {:ok, {false, "Bool", _}} = TE.eval("5 - 2 > 2 + 1")
-      assert {:ok, {true, "Bool", _}} = TE.eval("5 - 2 >= 2 + 1")
+      assert {:ok, {false, :Bool, _}} = TE.eval("1 + 2 < 4 - 1")
+      assert {:ok, {true, :Bool, _}} = TE.eval("1 + 2 <= 4 - 1")
+      assert {:ok, {false, :Bool, _}} = TE.eval("5 - 2 > 2 + 1")
+      assert {:ok, {true, :Bool, _}} = TE.eval("5 - 2 >= 2 + 1")
     end
 
     # TODO: Test using evaluation instead of AST as soon as == and != support booleans.
     #   Replace the content of this test with something like:
-    #     assert {:ok, {true, "Bool", _}} = TE.eval("1 < 1 == 2 > 2")
-    #     assert {:ok, {true, "Bool", _}} = TE.eval("1 <= 1 != 2 >= 3")
+    #     assert {:ok, {true, :Bool, _}} = TE.eval("1 < 1 == 2 > 2")
+    #     assert {:ok, {true, :Bool, _}} = TE.eval("1 <= 1 != 2 >= 3")
     test "comparison operators == and != have less precedence than <, >, <=, >=" do
       # Just for now, we test the parser for precedence
       assert {
@@ -122,50 +124,50 @@ defmodule Fika.ParserTest do
              } == TestParser.expression!("1 <= 1 != 2 >= 3")
 
       # Then we test == and != actually work
-      assert {:ok, {true, "Bool", _}} = TE.eval("1 == 1")
-      assert {:ok, {false, "Bool", _}} = TE.eval("1 != 1")
+      assert {:ok, {true, :Bool, _}} = TE.eval("1 == 1")
+      assert {:ok, {false, :Bool, _}} = TE.eval("1 != 1")
     end
 
     test "boolean & has less precedence than == and !=" do
-      assert {:ok, {true, "Bool", _}} = TE.eval("1 == 1 & 1 != 2")
-      assert {:ok, {false, "Bool", _}} = TE.eval("2 == 1 & 1 != 2")
+      assert {:ok, {true, :Bool, _}} = TE.eval("1 == 1 & 1 != 2")
+      assert {:ok, {false, :Bool, _}} = TE.eval("2 == 1 & 1 != 2")
     end
 
     test "boolean | has less precedence than &" do
-      assert {:ok, {true, "Bool", _}} = TE.eval("true | true & false")
-      assert {:ok, {true, "Bool", _}} = TE.eval("false & :true | true")
+      assert {:ok, {true, :Bool, _}} = TE.eval("true | true & false")
+      assert {:ok, {true, :Bool, _}} = TE.eval("false & :true | true")
     end
 
     test "expressions in parenthesis have higher precedence than anything else" do
-      assert {:ok, {1, "Int", _}} = TE.eval("-(-1)")
-      assert {:ok, {6, "Int", _}} = TE.eval("(3 - 1) * (1 + 2)")
-      assert {:ok, {true, "Bool", _}} = TE.eval("2 >= (1 + 1)")
-      assert {:ok, {true, "Bool", _}} = TE.eval("true & (false | true)")
+      assert {:ok, {1, :Int, _}} = TE.eval("-(-1)")
+      assert {:ok, {6, :Int, _}} = TE.eval("(3 - 1) * (1 + 2)")
+      assert {:ok, {true, :Bool, _}} = TE.eval("2 >= (1 + 1)")
+      assert {:ok, {true, :Bool, _}} = TE.eval("true & (false | true)")
     end
   end
 
   describe "operators with spaces" do
     # Unary operators' operand doesn't necessarily have to reside on the same line of the operator
     test "both vertical and horizontal space allowed between unary operators and their operand" do
-      assert {:ok, {false, "Bool", _}} = TE.eval("!#{TP.space()}true")
-      assert {:ok, {-1, "Int", _}} = TE.eval("-#{TP.space()}1")
+      assert {:ok, {false, :Bool, _}} = TE.eval("!#{TP.space()}true")
+      assert {:ok, {-1, :Int, _}} = TE.eval("-#{TP.space()}1")
     end
 
     # Horizontal space allowed between first operand and binary operator
     # Both vertical and horizontal space allowed between binary operator and second operand
     test "allowed spaces between binary operators and their operands" do
-      assert {:ok, {12, "Int", _}} = TE.eval("3#{TP.h_space()}*#{TP.space()}4")
-      assert {:ok, {2.0, "Float", _}} = TE.eval("4#{TP.h_space()}/#{TP.space()}2")
-      assert {:ok, {7, "Int", _}} = TE.eval("3#{TP.h_space()}+#{TP.space()}4")
-      assert {:ok, {1, "Int", _}} = TE.eval("3#{TP.h_space()}-#{TP.space()}2")
-      assert {:ok, {true, "Bool", _}} = TE.eval("2#{TP.h_space()}<#{TP.space()}3")
-      assert {:ok, {true, "Bool", _}} = TE.eval("2#{TP.h_space()}<=#{TP.space()}3")
-      assert {:ok, {true, "Bool", _}} = TE.eval("2#{TP.h_space()}>#{TP.space()}1")
-      assert {:ok, {true, "Bool", _}} = TE.eval("2#{TP.h_space()}>=#{TP.space()}1")
-      assert {:ok, {true, "Bool", _}} = TE.eval("2#{TP.h_space()}==#{TP.space()}2")
-      assert {:ok, {true, "Bool", _}} = TE.eval("2#{TP.h_space()}!=#{TP.space()}1")
-      assert {:ok, {true, "Bool", _}} = TE.eval("true#{TP.h_space()}&#{TP.space()}true")
-      assert {:ok, {true, "Bool", _}} = TE.eval("false#{TP.h_space()}|#{TP.space()}true")
+      assert {:ok, {12, :Int, _}} = TE.eval("3#{TP.h_space()}*#{TP.space()}4")
+      assert {:ok, {2.0, :Float, _}} = TE.eval("4#{TP.h_space()}/#{TP.space()}2")
+      assert {:ok, {7, :Int, _}} = TE.eval("3#{TP.h_space()}+#{TP.space()}4")
+      assert {:ok, {1, :Int, _}} = TE.eval("3#{TP.h_space()}-#{TP.space()}2")
+      assert {:ok, {true, :Bool, _}} = TE.eval("2#{TP.h_space()}<#{TP.space()}3")
+      assert {:ok, {true, :Bool, _}} = TE.eval("2#{TP.h_space()}<=#{TP.space()}3")
+      assert {:ok, {true, :Bool, _}} = TE.eval("2#{TP.h_space()}>#{TP.space()}1")
+      assert {:ok, {true, :Bool, _}} = TE.eval("2#{TP.h_space()}>=#{TP.space()}1")
+      assert {:ok, {true, :Bool, _}} = TE.eval("2#{TP.h_space()}==#{TP.space()}2")
+      assert {:ok, {true, :Bool, _}} = TE.eval("2#{TP.h_space()}!=#{TP.space()}1")
+      assert {:ok, {true, :Bool, _}} = TE.eval("true#{TP.h_space()}&#{TP.space()}true")
+      assert {:ok, {true, :Bool, _}} = TE.eval("false#{TP.h_space()}|#{TP.space()}true")
     end
 
     # Vertical space forbidden between first operand and binary operator
@@ -200,7 +202,7 @@ defmodule Fika.ParserTest do
       assert {
                :function,
                [position: {4, 20, 23}],
-               {:foo, [], {:type, {1, 0, 6}, "Nothing"},
+               {:foo, [], {:type, {1, 0, 6}, :Nothing},
                 [
                   {:identifier, {2, 10, 13}, :x},
                   {:call, {:-, {3, 14, 19}}, [{:identifier, {3, 14, 19}, :y}], :kernel}
@@ -272,15 +274,19 @@ defmodule Fika.ParserTest do
       end
       """
 
-      assert {
-               :function,
-               [position: {4, 20, 23}],
-               {:foo, [], {:type, {1, 0, 6}, "Nothing"},
-                [
-                  {:identifier, {2, 10, 13}, :x},
-                  {:call, {:-, {3, 14, 19}}, [{:identifier, {3, 14, 19}, :y}], :kernel}
-                ]}
-             } == TestParser.function_def!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.function_def(str)
+
+      assert result == [
+               {
+                 :function,
+                 [position: {4, 20, 23}],
+                 {:foo, [], {:type, {1, 0, 6}, :Nothing},
+                  [
+                    {:identifier, {2, 10, 13}, :x},
+                    {:call, {:-, {3, 14, 19}}, [{:identifier, {3, 14, 19}, :y}], :kernel}
+                  ]}
+               }
+             ]
     end
 
     test "+ and - are parsed as binary operators when on the same line of the first operand" do
@@ -291,18 +297,22 @@ defmodule Fika.ParserTest do
       end
       """
 
-      assert {
-               :function,
-               [position: {4, 20, 23}],
-               {:foo, [], {:type, {1, 0, 6}, "Nothing"},
-                [
-                  {:call, {:-, {3, 16, 19}},
-                   [
-                     {:identifier, {2, 10, 13}, :x},
-                     {:identifier, {3, 16, 19}, :y}
-                   ], :kernel}
-                ]}
-             } == TestParser.function_def!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.function_def(str)
+
+      assert result == [
+               {
+                 :function,
+                 [position: {4, 20, 23}],
+                 {:foo, [], {:type, {1, 0, 6}, :Nothing},
+                  [
+                    {:call, {:-, {3, 16, 19}},
+                     [
+                       {:identifier, {2, 10, 13}, :x},
+                       {:identifier, {3, 16, 19}, :y}
+                     ], :kernel}
+                  ]}
+               }
+             ]
     end
 
     test "add/sub has less precedence than mult/div" do
@@ -411,9 +421,12 @@ defmodule Fika.ParserTest do
       end
       """
 
-      assert {:function, [position: {3, 16, 19}],
-              {:foo, [], {:type, {1, 0, 6}, "Nothing"}, [{:integer, {2, 10, 15}, 123}]}} ==
-               TestParser.function_def!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.function_def(str)
+
+      assert result == [
+               {:function, [position: {3, 16, 19}],
+                {:foo, [], {:type, {1, 0, 6}, :Nothing}, [{:integer, {2, 10, 15}, 123}]}}
+             ]
     end
 
     test "with return type Int" do
@@ -423,9 +436,12 @@ defmodule Fika.ParserTest do
       end
       """
 
-      assert {:function, [position: {3, 22, 25}],
-              {:foo, [], {:type, {1, 0, 12}, "Int"}, [{:integer, {2, 16, 21}, 123}]}} ==
-               TestParser.function_def!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.function_def(str)
+
+      assert result == [
+               {:function, [position: {3, 22, 25}],
+                {:foo, [], {:type, {1, 0, 12}, :Int}, [{:integer, {2, 16, 21}, 123}]}}
+             ]
     end
 
     test "with return type :ok" do
@@ -435,23 +451,32 @@ defmodule Fika.ParserTest do
       end
       """
 
-      assert {:function, [position: {3, 22, 25}],
-              {:foo, [], {:type, {1, 0, 12}, ":ok"}, [{:integer, {2, 16, 21}, 123}]}} ==
-               TestParser.function_def!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.function_def(str)
+
+      assert result == [
+               {:function, [position: {3, 22, 25}],
+                {:foo, [], {:type, {1, 0, 12}, :ok}, [{:integer, {2, 16, 21}, 123}]}}
+             ]
     end
 
     test "with type params" do
       str = """
-      fn foo(a: List(String)) : List(Int) do
+      fn foo(a: List(List(List(String)))) : List(Nothing) do
         x
       end
       """
 
-      assert {:function, [position: {3, 43, 46}],
-              {:foo, [{{:identifier, {1, 0, 8}, :a}, {:type, {1, 0, 22}, "List(String)"}}],
-               {:type, {1, 0, 35}, "List(Int)"},
-               [{:identifier, {2, 39, 42}, :x}]}} ==
-               TestParser.function_def!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.function_def(str)
+
+      assert result == [
+               {:function, [position: {3, 59, 62}],
+                {:foo,
+                 [
+                   {{:identifier, {1, 0, 8}, :a},
+                    {:type, {1, 0, 34}, %T.List{type: %T.List{type: %T.List{type: :String}}}}}
+                 ], {:type, {1, 0, 51}, %T.List{type: :Nothing}},
+                 [{:identifier, {2, 55, 58}, :x}]}}
+             ]
     end
 
     test "with args" do
@@ -461,17 +486,20 @@ defmodule Fika.ParserTest do
       end
       """
 
-      assert {:function, [position: {3, 40, 43}],
-              {:foo,
-               [
-                 {{:identifier, {1, 0, 8}, :x}, {:type, {1, 0, 13}, "Int"}},
-                 {{:identifier, {1, 0, 16}, :y}, {:type, {1, 0, 21}, "Int"}}
-               ], {:type, {1, 0, 28}, "Int"},
-               [
-                 {:call, {:+, {2, 32, 39}},
-                  [{:identifier, {2, 32, 35}, :x}, {:identifier, {2, 32, 39}, :y}], :kernel}
-               ]}} ==
-               TestParser.function_def!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.function_def(str)
+
+      assert result == [
+               {:function, [position: {3, 40, 43}],
+                {:foo,
+                 [
+                   {{:identifier, {1, 0, 8}, :x}, {:type, {1, 0, 13}, :Int}},
+                   {{:identifier, {1, 0, 16}, :y}, {:type, {1, 0, 21}, :Int}}
+                 ], {:type, {1, 0, 28}, :Int},
+                 [
+                   {:call, {:+, {2, 32, 39}},
+                    [{:identifier, {2, 32, 35}, :x}, {:identifier, {2, 32, 39}, :y}], :kernel}
+                 ]}}
+             ]
     end
   end
 
@@ -801,31 +829,53 @@ defmodule Fika.ParserTest do
     test "types with no args" do
       str = "Int"
 
-      assert {:type, {1, 0, 3}, "Int"} == TestParser.type_str!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.type_str(str)
+
+      assert result == [{:type, {1, 0, 3}, :Int}]
     end
 
     test "parses types with an arg" do
       str = "List(Int)"
 
-      assert {:type, {1, 0, 9}, "List(Int)"} == TestParser.type_str!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.type_str(str)
+
+      assert result == [{:type, {1, 0, 9}, %T.List{type: :Int}}]
     end
 
     test "parses types with nested args" do
       str = "List(List(String))"
 
-      assert {:type, {1, 0, 18}, "List(List(String))"} == TestParser.type_str!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.type_str(str)
+
+      assert result == [{:type, {1, 0, 18}, %T.List{type: %T.List{type: :String}}}]
     end
 
     test "parses function type with no args" do
       str = "Fn(-> Int)"
 
-      assert {:type, {1, 0, 10}, "Fn(->Int)"} == TestParser.type_str!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.type_str(str)
+
+      assert result == [
+               {:type, {1, 0, 10},
+                %T.FunctionRef{
+                  arg_types: [],
+                  return_type: :Int
+                }}
+             ]
     end
 
     test "parses function type with args" do
       str = "Fn(Int, Int -> Int)"
 
-      assert {:type, {1, 0, 19}, "Fn(Int,Int->Int)"} == TestParser.type_str!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.type_str(str)
+
+      assert result == [
+               {:type, {1, 0, 19},
+                %T.FunctionRef{
+                  arg_types: [:Int, :Int],
+                  return_type: :Int
+                }}
+             ]
     end
 
     test "record type" do
@@ -837,13 +887,15 @@ defmodule Fika.ParserTest do
     test "atom type" do
       str = ":foo"
 
-      assert {:type, {1, 0, 4}, ":foo"} == TestParser.type_str!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.type_str(str)
+      assert result == [{:type, {1, 0, 4}, :foo}]
     end
 
     test "list of atom" do
       str = "List(:foo)"
 
-      assert {:type, {1, 0, 10}, "List(:foo)"} == TestParser.type_str!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.type_str(str)
+      assert result == [{:type, {1, 0, 10}, %T.List{type: :foo}}]
     end
   end
 
@@ -853,7 +905,10 @@ defmodule Fika.ParserTest do
       &foo
       """
 
-      assert {:function_ref, {1, 0, 4}, {nil, :foo, []}} == TestParser.expression!(str)
+      result = TestParser.expression!(str)
+
+      assert result ==
+               {:function_ref, {1, 0, 4}, {nil, :foo, []}}
     end
 
     test "parses a remote function ref with no args" do
@@ -861,12 +916,10 @@ defmodule Fika.ParserTest do
       &foo.bar
       """
 
-      assert {:function_ref, {1, 0, 8},
-              {
-                :foo,
-                :bar,
-                []
-              }} == TestParser.expression!(str)
+      result = TestParser.expression!(str)
+
+      assert result ==
+               {:function_ref, {1, 0, 8}, {:foo, :bar, []}}
     end
 
     test "parses a function ref with arg types" do
@@ -874,15 +927,10 @@ defmodule Fika.ParserTest do
       &foo.bar(Int, Int)
       """
 
-      assert {:function_ref, {1, 0, 18},
-              {
-                :foo,
-                :bar,
-                [
-                  "Int",
-                  "Int"
-                ]
-              }} == TestParser.expression!(str)
+      result = TestParser.expression!(str)
+
+      assert result ==
+               {:function_ref, {1, 0, 18}, {:foo, :bar, [:Int, :Int]}}
     end
   end
 
@@ -895,9 +943,12 @@ defmodule Fika.ParserTest do
       end
       """
 
-      assert {:function, [position: {4, 36, 39}],
-              {:foo, [], {:type, {2, 20, 26}, "Nothing"}, [{:integer, {3, 30, 35}, 123}]}} ==
-               TestParser.function_def!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.function_def(str)
+
+      assert result == [
+               {:function, [position: {4, 36, 39}],
+                {:foo, [], {:type, {2, 20, 26}, :Nothing}, [{:integer, {3, 30, 35}, 123}]}}
+             ]
     end
 
     test "At end of line" do
@@ -907,9 +958,12 @@ defmodule Fika.ParserTest do
       end # Comment 3
       """
 
-      assert {:function, [position: {3, 40, 43}],
-              {:foo, [], {:type, {1, 0, 6}, "Nothing"}, [{:integer, {2, 22, 27}, 123}]}} ==
-               TestParser.function_def!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.function_def(str)
+
+      assert result == [
+               {:function, [position: {3, 40, 43}],
+                {:foo, [], {:type, {1, 0, 6}, :Nothing}, [{:integer, {2, 22, 27}, 123}]}}
+             ]
     end
 
     test "Can't appear in between characters" do
@@ -929,9 +983,12 @@ defmodule Fika.ParserTest do
       end
       """
 
-      assert {:function, [position: {3, 22, 25}],
-              {:foo, [], {:type, {1, 0, 6}, "Nothing"}, [{:string, {2, 10, 21}, ["foo#bar"]}]}} ==
-               TestParser.function_def!(str)
+      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.function_def(str)
+
+      assert result == [
+               {:function, [position: {3, 22, 25}],
+                {:foo, [], {:type, {1, 0, 6}, :Nothing}, [{:string, {2, 10, 21}, ["foo#bar"]}]}}
+             ]
     end
   end
 
diff --git a/test/fika/type_checker_test.exs b/test/fika/type_checker_test.exs
index a8a5f87..0881d62 100644
--- a/test/fika/type_checker_test.exs
+++ b/test/fika/type_checker_test.exs
@@ -6,14 +6,14 @@ defmodule Fika.TypeCheckerTest do
     TypeChecker
   }
 
-  alias Fika.Types.FunctionRef
+  alias Fika.Types, as: T
 
   test "infer type of integer literals" do
     str = "123"
 
     ast = TestParser.expression!(str)
 
-    assert {:ok, "Int", _} = TypeChecker.infer_exp(Env.init(), ast)
+    assert {:ok, :Int, _} = TypeChecker.infer_exp(Env.init(), ast)
   end
 
   test "infer type of atom expressions" do
@@ -21,7 +21,7 @@ defmodule Fika.TypeCheckerTest do
 
     {:atom, {1, 0, 2}, :a} = ast = TestParser.expression!(str)
 
-    assert {:ok, ":a", _} = TypeChecker.infer_exp(Env.init(), ast)
+    assert {:ok, :a, _} = TypeChecker.infer_exp(Env.init(), ast)
   end
 
   test "infer type for list of atom expressions" do
@@ -29,7 +29,7 @@ defmodule Fika.TypeCheckerTest do
 
     ast = TestParser.expression!(str)
 
-    assert {:ok, "List(:a)", _} = TypeChecker.infer_exp(Env.init(), ast)
+    assert {:ok, %T.List{type: :a}, _} = TypeChecker.infer_exp(Env.init(), ast)
   end
 
   test "infer type of arithmetic expressions" do
@@ -37,7 +37,7 @@ defmodule Fika.TypeCheckerTest do
 
     ast = TestParser.expression!(str)
 
-    assert {:ok, "Int", _} = TypeChecker.infer_exp(Env.init(), ast)
+    assert {:ok, :Int, _} = TypeChecker.infer_exp(Env.init(), ast)
   end
 
   describe "logical operators" do
@@ -45,23 +45,23 @@ defmodule Fika.TypeCheckerTest do
       # and
       str = "true & false"
       ast = TestParser.expression!(str)
-      assert {:ok, "Bool", _} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, :Bool, _} = TypeChecker.infer_exp(Env.init(), ast)
 
       # or
       str = "true | false"
       ast = TestParser.expression!(str)
-      assert {:ok, "Bool", _} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, :Bool, _} = TypeChecker.infer_exp(Env.init(), ast)
 
       # negation
       str = "!true"
       ast = TestParser.expression!(str)
-      assert {:ok, "Bool", _} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, :Bool, _} = TypeChecker.infer_exp(Env.init(), ast)
     end
 
     test "infer type of logical expressions when using atoms" do
       str = "true & :false"
       ast = TestParser.expression!(str)
-      assert {:ok, "Bool", _} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, :Bool, _} = TypeChecker.infer_exp(Env.init(), ast)
     end
   end
 
@@ -86,7 +86,7 @@ defmodule Fika.TypeCheckerTest do
       Env.init()
       |> Env.init_module_env("test", ast)
 
-    assert {:ok, "Int", _} = TypeChecker.infer(function, env)
+    assert {:ok, :Int, _} = TypeChecker.infer(function, env)
   end
 
   test "check returns error when return type is not the inferred type" do
@@ -138,10 +138,10 @@ defmodule Fika.TypeCheckerTest do
       Env.init()
       |> Env.init_module_env("test", ast)
 
-    assert {:ok, "Int", env} = TypeChecker.infer(foo, env)
+    assert {:ok, :Int, env} = TypeChecker.infer(foo, env)
 
-    assert Env.get_function_type(env, "test.foo(Int)") == "Int"
-    assert Env.get_function_type(env, "test.bar(Int)") == "Int"
+    assert Env.get_function_type(env, "test.foo(Int)") == :Int
+    assert Env.get_function_type(env, "test.bar(Int)") == :Int
   end
 
   test "infer function with variable assignments which get used in function calls" do
@@ -158,14 +158,14 @@ defmodule Fika.TypeCheckerTest do
     env =
       Env.init()
       |> Env.init_module_env("test", ast)
-      |> Env.add_function_type("test2.div(Float,Int)", "Float")
-      |> Env.add_function_type("test2.div(Int,Float)", "Float")
-      |> Env.add_function_type("test2.add(Float,Int)", "Float")
-      |> Env.add_function_type("test2.add(Int,Float)", "Float")
-      |> Env.add_function_type("test2.add(Int,Int)", "Int")
-
-    assert {:ok, "Float", _} = TypeChecker.infer(function, env)
-    assert {:ok, "Float", _} = TypeChecker.check(function, env)
+      |> Env.add_function_type("test2.div(Float, Int)", :Float)
+      |> Env.add_function_type("test2.div(Int, Float)", :Float)
+      |> Env.add_function_type("test2.add(Float, Int)", :Float)
+      |> Env.add_function_type("test2.add(Int, Float)", :Float)
+      |> Env.add_function_type("test2.add(Int, Int)", :Int)
+
+    assert {:ok, :Float, _} = TypeChecker.infer(function, env)
+    assert {:ok, :Float, _} = TypeChecker.check(function, env)
   end
 
   describe "string" do
@@ -174,7 +174,7 @@ defmodule Fika.TypeCheckerTest do
 
       ast = TestParser.expression!(str)
 
-      assert {:ok, "String", _} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, :String, _} = TypeChecker.infer_exp(Env.init(), ast)
     end
 
     test "parses string inside interpolation" do
@@ -184,7 +184,7 @@ defmodule Fika.TypeCheckerTest do
 
       ast = TestParser.expression!(str)
 
-      assert {:ok, "String", _} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, :String, _} = TypeChecker.infer_exp(Env.init(), ast)
     end
 
     test "returns error when unknown variable in string interpolation" do
@@ -207,7 +207,7 @@ defmodule Fika.TypeCheckerTest do
 
       env = Env.init_module_env(Env.init(), "test", ast)
 
-      assert {:ok, "String", _} = TypeChecker.infer_block(env, ast)
+      assert {:ok, :String, _} = TypeChecker.infer_block(env, ast)
     end
 
     test "accepts only strings in interpolation" do
@@ -230,7 +230,7 @@ defmodule Fika.TypeCheckerTest do
 
       ast = TestParser.expression!(str)
 
-      assert {:ok, "List(Int)", _} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, %T.List{type: :Int}, _} = TypeChecker.infer_exp(Env.init(), ast)
     end
 
     test "list of integers and floats" do
@@ -247,7 +247,7 @@ defmodule Fika.TypeCheckerTest do
 
       ast = TestParser.expression!(str)
 
-      assert {:ok, "List(Float)", _} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, %T.List{type: :Float}, _} = TypeChecker.infer_exp(Env.init(), ast)
     end
 
     test "List of strings" do
@@ -255,7 +255,7 @@ defmodule Fika.TypeCheckerTest do
 
       ast = TestParser.expression!(str)
 
-      assert {:ok, "List(String)", _} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, %T.List{type: :String}, _} = TypeChecker.infer_exp(Env.init(), ast)
     end
 
     test "List of list of integers" do
@@ -263,7 +263,7 @@ defmodule Fika.TypeCheckerTest do
 
       ast = TestParser.expression!(str)
 
-      assert {:ok, "List(List(Int))", _} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, %T.List{type: %T.List{type: :Int}}, _} = TypeChecker.infer_exp(Env.init(), ast)
     end
 
     test "empty list" do
@@ -271,7 +271,7 @@ defmodule Fika.TypeCheckerTest do
 
       ast = TestParser.expression!(str)
 
-      assert {:ok, "List(Nothing)", _} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, %T.List{type: :Nothing}, _} = TypeChecker.infer_exp(Env.init(), ast)
     end
   end
 
@@ -281,7 +281,8 @@ defmodule Fika.TypeCheckerTest do
 
       ast = TestParser.expression!(str)
 
-      assert {:ok, "{Int,Int,Int}", _env} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, %T.Tuple{elements: [:Int, :Int, :Int]}, _env} =
+               TypeChecker.infer_exp(Env.init(), ast)
     end
 
     test "tuple of integers and floats" do
@@ -289,7 +290,8 @@ defmodule Fika.TypeCheckerTest do
 
       ast = TestParser.expression!(str)
 
-      assert {:ok, "{Int,Float,Int}", _env} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, %T.Tuple{elements: [:Int, :Float, :Int]}, _env} =
+               TypeChecker.infer_exp(Env.init(), ast)
     end
 
     test "tuple of floats inferred from fn calls" do
@@ -297,7 +299,8 @@ defmodule Fika.TypeCheckerTest do
 
       ast = TestParser.expression!(str)
 
-      assert {:ok, "{Float,Float}", _env} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, %T.Tuple{elements: [:Float, :Float]}, _env} =
+               TypeChecker.infer_exp(Env.init(), ast)
     end
 
     test "tuple of strings" do
@@ -305,7 +308,8 @@ defmodule Fika.TypeCheckerTest do
 
       ast = TestParser.expression!(str)
 
-      assert {:ok, "{String,String}", _env} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, %T.Tuple{elements: [:String, :String]}, _env} =
+               TypeChecker.infer_exp(Env.init(), ast)
     end
 
     test "tuple of tuple of mixed types" do
@@ -313,7 +317,13 @@ defmodule Fika.TypeCheckerTest do
 
       ast = TestParser.expression!(str)
 
-      assert {:ok, "{{Int,Float},{String,Bool}}", _env} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok,
+              %T.Tuple{
+                elements: [
+                  %T.Tuple{elements: [:Int, :Float]},
+                  %T.Tuple{elements: [:String, :Bool]}
+                ]
+              }, _env} = TypeChecker.infer_exp(Env.init(), ast)
     end
   end
 
@@ -323,7 +333,8 @@ defmodule Fika.TypeCheckerTest do
 
       ast = TestParser.expression!(str)
 
-      assert {:ok, "{foo:Int,bar:String}", _} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, %T.Record{fields: [bar: :String, foo: :Int]}, _} =
+               TypeChecker.infer_exp(Env.init(), ast)
     end
 
     test "error" do
@@ -341,7 +352,8 @@ defmodule Fika.TypeCheckerTest do
 
       ast = TestParser.expression!(str)
 
-      assert {:ok, "Map(String,Int)", _} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, %T.Map{key_type: :String, value_type: :Int}, _} =
+               TypeChecker.infer_exp(Env.init(), ast)
     end
 
     test "type check for map with mixed type" do
@@ -369,10 +381,13 @@ defmodule Fika.TypeCheckerTest do
       env =
         Env.init()
         |> Env.init_module_env("test", ast)
-        |> Env.add_function_type("bar.sum(Int,Int)", "Int")
+        |> Env.add_function_type("bar.sum(Int, Int)", :Int)
 
-      assert {:ok, %FunctionRef{arg_types: ["Int", "Int"], return_type: "Int"}, _} =
-               TypeChecker.infer_exp(env, ast)
+      assert {:ok,
+              %T.FunctionRef{
+                arg_types: [:Int, :Int],
+                return_type: :Int
+              }, _} = TypeChecker.infer_exp(env, ast)
     end
 
     test "without args" do
@@ -382,9 +397,9 @@ defmodule Fika.TypeCheckerTest do
       env =
         Env.init()
         |> Env.init_module_env("test", ast)
-        |> Env.add_function_type("bar.sum()", "Int")
+        |> Env.add_function_type("bar.sum()", :Int)
 
-      assert {:ok, %FunctionRef{arg_types: [], return_type: "Int"}, _} =
+      assert {:ok, %T.FunctionRef{arg_types: [], return_type: :Int}, _} =
                TypeChecker.infer_exp(env, ast)
     end
   end
@@ -394,14 +409,14 @@ defmodule Fika.TypeCheckerTest do
       str = "true"
       ast = TestParser.expression!(str)
 
-      assert {:ok, "Bool", _} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, :Bool, _} = TypeChecker.infer_exp(Env.init(), ast)
     end
 
     test "false" do
       str = "false"
       ast = TestParser.expression!(str)
 
-      assert {:ok, "Bool", _} = TypeChecker.infer_exp(Env.init(), ast)
+      assert {:ok, :Bool, _} = TypeChecker.infer_exp(Env.init(), ast)
     end
   end
 
@@ -436,7 +451,7 @@ defmodule Fika.TypeCheckerTest do
       ast = TestParser.expression!(str)
       env = Env.init_module_env(Env.init(), "test", ast)
 
-      assert {:ok, "String", _env} = TypeChecker.infer_exp(env, ast)
+      assert {:ok, :String, _env} = TypeChecker.infer_exp(env, ast)
     end
 
     test "error when if and else blocks have different return types" do
@@ -470,7 +485,7 @@ defmodule Fika.TypeCheckerTest do
       ast = TestParser.expression!(str)
       env = Env.init_module_env(Env.init(), "test", ast)
 
-      assert {:ok, "String", _env} = TypeChecker.infer_exp(env, ast)
+      assert {:ok, :String, _env} = TypeChecker.infer_exp(env, ast)
     end
   end
 
@@ -488,9 +503,9 @@ defmodule Fika.TypeCheckerTest do
       env =
         Env.init()
         |> Env.init_module_env("test", ast)
-        |> Env.add_function_type("test2.bar(String,Int)", "Bool")
+        |> Env.add_function_type("test2.bar(String, Int)", :Bool)
 
-      assert {:ok, "Bool", _} = TypeChecker.infer(function, env)
+      assert {:ok, :Bool, _} = TypeChecker.infer(function, env)
     end
 
     test "identifier is not a reference" do
@@ -524,10 +539,10 @@ defmodule Fika.TypeCheckerTest do
       env =
         Env.init()
         |> Env.init_module_env("test", ast)
-        |> Env.add_function_type("test2.bar(String,Int)", "Bool")
+        |> Env.add_function_type("test2.bar(String, Int)", :Bool)
 
       error =
-        "Expected function reference to be called with arguments (String,Int), but it was called with arguments (Int)"
+        "Expected function reference to be called with arguments (String, Int), but it was called with arguments (Int)"
 
       assert {:error, ^error} = TypeChecker.infer(function, env)
     end
diff --git a/test/support/test_parser.ex b/test/support/test_parser.ex
index fe5f862..5cf3895 100644
--- a/test/support/test_parser.ex
+++ b/test/support/test_parser.ex
@@ -49,6 +49,11 @@ defmodule TestParser do
 
   defparsec :expression, exp |> concat(allow_space) |> eos()
   defparsec :function_def, function_def
-  defparsec :type_str, parse_type |> concat(allow_space) |> eos()
+
+  defparsec :type_str,
+            parse_type
+            |> concat(allow_space)
+            |> eos()
+
   defparsec :exps, exps |> concat(allow_space) |> eos()
 end