Repository: fika-lang/fika (elixir)

Issue:
Add tuples
Tuples are of the form `{"foo", 123}` which is of the type: `{String, Int}`. They are like lists, but what they will contain is already fixed at compile time and cannot contain arbitrary values.

Pull request description:
Add support for Tuples
Closes #12 

Patch:
diff --git a/README.md b/README.md
index 17840f9..fd106ee 100644
--- a/README.md
+++ b/README.md
@@ -95,6 +95,9 @@ x = true
 # Type: List(Int)
 list_of_ints = [1, 2, 3]
 
+# Type: {String,Bool}
+tuple = {"tuple", true}
+
 # Type: {foo: Int}
 record = {foo: 123}
 ```
diff --git a/example.fi b/example.fi
index f41b84e..eea252a 100644
--- a/example.fi
+++ b/example.fi
@@ -33,3 +33,7 @@ fn simple_if : String do
     "you can't see me \m/"
   end
 end
+
+fn tuple : {Int,Bool} do
+  {1, true}
+end
diff --git a/lib/fika/erl_translate.ex b/lib/fika/erl_translate.ex
index 0a8a773..cda3ad5 100644
--- a/lib/fika/erl_translate.ex
+++ b/lib/fika/erl_translate.ex
@@ -62,7 +62,7 @@ defmodule Fika.ErlTranslate do
   defp translate_exp({:atom, {line, _, _}, value}) do
     {:atom, line, value}
   end
-  
+
   defp translate_exp({{:=, {line, _, _}}, pattern, exp}) do
     {:match, line, translate_exp(pattern), translate_exp(exp)}
   end
@@ -83,6 +83,10 @@ defmodule Fika.ErlTranslate do
     do_translate_list(value, line)
   end
 
+  defp translate_exp({:tuple, {line, _, _}, value}) do
+    {:tuple, line, translate_exps(value)}
+  end
+
   defp translate_exp({:record, {line, _, _}, name, k_vs}) do
     k_vs = Enum.map(k_vs, fn {{:identifier, {l, _, _}, k}, v} ->
       {:map_field_assoc, l, {:atom, l, k}, translate_exp(v)}
diff --git a/lib/fika/parser.ex b/lib/fika/parser.ex
index 04e71d1..d3264d8 100644
--- a/lib/fika/parser.ex
+++ b/lib/fika/parser.ex
@@ -127,6 +127,14 @@ defmodule Fika.Parser do
     |> ignore(string("]"))
     |> Helper.to_ast(:exp_list)
 
+  tuple =
+    ignore(string("{"))
+    |> concat(allow_space)
+    |> concat(list_content)
+    |> concat(allow_space)
+    |> ignore(string("}"))
+    |> Helper.to_ast(:tuple)
+
   exp_paren =
     ignore(string("("))
     |> parsec(:exp)
@@ -230,6 +238,7 @@ defmodule Fika.Parser do
       boolean,
       string_exp,
       exp_list,
+      tuple,
       record,
       function_ref,
       atom,
@@ -306,6 +315,29 @@ defmodule Fika.Parser do
     |> concat(allow_space)
     |> string(")")
 
+  # To parse functions with tuple return type
+  type_tuple_element =
+    allow_space
+    |> 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)
+    |> string("}")
+    |> reduce({Enum, :join, []})
+    |> label("tuple type")
+
   type_key_value =
     allow_space
     |> concat(identifier_str)
@@ -349,7 +381,9 @@ defmodule Fika.Parser do
       simple_type
       |> optional(type_parens),
       record_type,
-      atom
+      atom,
+      record_type,
+      tuple_type
     ])
 
   parse_type =
diff --git a/lib/fika/parser_helper.ex b/lib/fika/parser_helper.ex
index 51b6765..d328083 100644
--- a/lib/fika/parser_helper.ex
+++ b/lib/fika/parser_helper.ex
@@ -105,6 +105,10 @@ defmodule Fika.ParserHelper do
     {:list, line, result}
   end
 
+  def do_to_ast({result, line}, :tuple) do
+    {:tuple, line, result}
+  end
+
   def do_to_ast({[k, v], _line}, :key_value) do
     {k, v}
   end
diff --git a/lib/fika/type_checker.ex b/lib/fika/type_checker.ex
index dea1889..412812f 100644
--- a/lib/fika/type_checker.ex
+++ b/lib/fika/type_checker.ex
@@ -149,6 +149,21 @@ defmodule Fika.TypeChecker do
     infer_list_exps(env, exps)
   end
 
+  # Tuple
+  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}
+      error ->
+        error
+    end
+  end
+
   # Record
   def infer_exp(env, {:record, _, name, key_values}) do
     if name do
@@ -252,6 +267,16 @@ defmodule Fika.TypeChecker do
     end)
   end
 
+  defp do_infer_tuple_exps(exps, env) do
+    Enum.reduce_while(exps, {:ok, [], env}, fn exp, {:ok, acc, env} ->
+      case infer_exp(env, exp) do
+        {:ok, exp_type, env} ->
+          {:cont, {:ok, [exp_type | acc], env}}
+        error -> {:halt, error}
+      end
+    end)
+  end
+
   def infer_args(env, exp, module) do
     case do_infer_args(env, exp) do
       {:ok, type_acc, env} ->


Test patch:
diff --git a/test/fika/erl_translate_test.exs b/test/fika/erl_translate_test.exs
index 7b76179..94cf799 100644
--- a/test/fika/erl_translate_test.exs
+++ b/test/fika/erl_translate_test.exs
@@ -141,4 +141,24 @@ defmodule Fika.ErlTranslateTest do
       ]
     } = result
   end
+
+  describe "tuple" do
+    test "single element tuple" do
+      str = "{1}"
+      ast = Fika.Parser.expression!(str)
+
+      assert {:tuple, 1, [{:integer, 1, 1}]} = ErlTranslate.translate_expression(ast)
+    end
+
+    test "tuple of tuples" do
+      str = "{{1}, {2}}"
+      ast = Fika.Parser.expression!(str)
+
+      assert {
+        :tuple,
+        1,
+        [{:tuple, 1, [{:integer, 1, 1}]}, {:tuple, 1, [{:integer, 1, 2}]}]
+      } = ErlTranslate.translate_expression(ast)
+    end
+  end
 end
diff --git a/test/fika/parser_test.exs b/test/fika/parser_test.exs
index b3fd603..9be9eb7 100644
--- a/test/fika/parser_test.exs
+++ b/test/fika/parser_test.exs
@@ -426,6 +426,50 @@ defmodule Fika.ParserTest do
     end
   end
 
+  describe "tuple" do
+    test "parses tuple with one element" do
+      str = """
+      {1}
+      """
+
+      {:ok, result, _rest, _context, _line, _byte_offset} = Parser.expression(str)
+      assert result == [{:tuple, {1, 0, 3}, [{:integer, {1, 0, 2}, 1}]}]
+    end
+
+    test "parses tuple with multiple elements" do
+      str = """
+      {1, 2, 3}
+      """
+
+      {:ok, result, _rest, _context, _line, _byte_offset} = Parser.expression(str)
+      assert result == [
+        {:tuple, {1, 0, 9}, [
+            {:integer, {1, 0, 2}, 1},
+            {:integer, {1, 0, 5}, 2},
+            {:integer, {1, 0, 8}, 3},
+          ]
+        }
+      ]
+    end
+
+    test "parses tuple with match exps" do
+      str = """
+      {a = 1, 2, 3}
+      """
+
+      {:ok, result, _rest, _context, _line, _byte_offset} = Parser.expression(str)
+
+      assert result == [
+        {:tuple, {1, 0, 13}, [
+            {{:=, {1, 0, 6}}, {:identifier, {1, 0, 2}, :a}, {:integer, {1, 0, 6}, 1}},
+            {:integer, {1, 0, 9}, 2},
+            {:integer, {1, 0, 12}, 3},
+          ]
+        }
+      ]
+    end
+  end
+
   describe "types" do
     test "types with no args" do
       str = "Int"
diff --git a/test/fika/type_checker_test.exs b/test/fika/type_checker_test.exs
index 5cce937..7f9a5bd 100644
--- a/test/fika/type_checker_test.exs
+++ b/test/fika/type_checker_test.exs
@@ -78,6 +78,22 @@ defmodule Fika.TypeCheckerTest do
     assert {:error, "Expected type: Float, got: Int"} = TypeChecker.check(function, env)
   end
 
+  test "checks tuple return type for function" do
+    str = """
+    fn foo : {Float} do
+      {2}
+    end
+    """
+
+    {:module, _, [function]} = ast = Fika.Parser.parse_module(str, "test")
+
+    env =
+      Env.init()
+      |> Env.init_module_env("test", ast)
+
+    assert {:error, "Expected type: {Float}, got: {Int}"} = TypeChecker.check(function, env)
+  end
+
   test "infer return type of another function in the module" do
     str = """
     fn foo(a: Int) : Int do
@@ -183,6 +199,48 @@ defmodule Fika.TypeCheckerTest do
     end
   end
 
+  describe "tuples" do
+    test "tuple of integers" do
+      str = "{1, 2, 3}"
+
+      ast = Fika.Parser.expression!(str)
+
+      assert {:ok, "{Int,Int,Int}", _env} = TypeChecker.infer_exp(Env.init(), ast)
+    end
+
+    test "tuple of integers and floats" do
+      str = "{1, 2/3, 3}"
+
+      ast = Fika.Parser.expression!(str)
+
+      assert {:ok,"{Int,Float,Int}", _env} = TypeChecker.infer_exp(Env.init(), ast)
+    end
+
+    test "tuple of floats inferred from fn calls" do
+      str = "{1/2, 2/3}"
+
+      ast = Fika.Parser.expression!(str)
+
+      assert {:ok, "{Float,Float}", _env} = TypeChecker.infer_exp(Env.init(), ast)
+    end
+
+    test "tuple of strings" do
+      str = ~s({"foo", "bar"})
+
+      ast = Fika.Parser.expression!(str)
+
+      assert {:ok, "{String,String}", _env} = TypeChecker.infer_exp(Env.init(), ast)
+    end
+
+    test "tuple of tuple of mixed types" do
+      str = ~s({{1, 2/5}, {"3", true}})
+
+      ast = Fika.Parser.expression!(str)
+
+      assert {:ok, "{{Int,Float},{String,Bool}}", _env} = TypeChecker.infer_exp(Env.init(), ast)
+    end
+  end
+
   describe "record" do
     test "unnamed record" do
       str = "{foo: 123, bar: \"Baz\"}"