Repository: fika-lang/fika
Pull request #44 (2020-11-02 02:14:35)

Parser fixes
Running `example.fi` led to the discovery of the following issues:
- the parser couldn't parse `Bool` data type e.g. in `fn foo : Bool`;
- maps and records types were represented in the AST using strings, causing failures in type checking.

Also added union types example (once again, I tried coming up with a "useful" one, let me know if you prefer something else).


Patch:
diff --git a/example.fi b/example.fi
index 6b7df1b..2331b67 100644
--- a/example.fi
+++ b/example.fi
@@ -87,4 +87,13 @@ fn max(a: Int, b: Int) : Int do
   else
     b
   end
+end
+
+# Union types
+fn interval(start: Int, finish: Int) : {:error, String} | {:ok, Int} do
+  if start <= finish do
+    {:ok, finish - start}
+  else
+    {:error, "Interval cannot end before it's started"}
+  end
 end
\ No newline at end of file
diff --git a/lib/fika/parser/helper.ex b/lib/fika/parser/helper.ex
index f0e2fc5..e94dfc2 100644
--- a/lib/fika/parser/helper.ex
+++ b/lib/fika/parser/helper.ex
@@ -71,6 +71,18 @@ defmodule Fika.Parser.Helper do
     %T.Tuple{elements: inner_types}
   end
 
+  def do_to_ast({[key_type, value_type], _line}, :map_type) do
+    %T.Map{key_type: key_type, value_type: value_type}
+  end
+
+  def do_to_ast({[key, value], _line}, :record_field) do
+    {String.to_atom(key), value}
+  end
+
+  def do_to_ast({fields, _line}, :record_type) do
+    %T.Record{fields: fields}
+  end
+
   def do_to_ast({[{:atom, _, atom}], _line}, :atom_type) do
     atom
   end
diff --git a/lib/fika/parser/types.ex b/lib/fika/parser/types.ex
index a901873..1185749 100644
--- a/lib/fika/parser/types.ex
+++ b/lib/fika/parser/types.ex
@@ -31,45 +31,43 @@ defmodule Fika.Parser.Types do
     |> ignore(string(")"))
     |> Helper.to_ast(:function_type)
 
-  type_key_value =
+  record_field =
     allow_space
     |> concat(identifier_str)
     |> concat(allow_space)
-    |> string(":")
+    |> ignore(string(":"))
     |> concat(allow_space)
     |> parsec(:type)
     |> label("key value pair")
-    |> reduce({Enum, :join, []})
+    |> Helper.to_ast(:record_field)
 
-  type_key_values =
-    type_key_value
+  record_fields =
+    record_field
     |> repeat(
       allow_space
       |> ignore(string(","))
       |> concat(allow_space)
-      |> concat(type_key_value)
+      |> concat(record_field)
     )
-    |> reduce({Enum, :join, [","]})
 
   record_type =
-    string("{")
-    |> concat(type_key_values)
-    |> string("}")
-    |> reduce({Enum, :join, []})
+    ignore(string("{"))
+    |> concat(record_fields)
+    |> ignore(string("}"))
     |> label("record type")
+    |> Helper.to_ast(:record_type)
 
-  # To parse functions with map return type
   map_type =
-    string("Map")
-    |> string("(")
+    ignore(string("Map("))
     |> parsec(:type)
     |> concat(allow_space)
-    |> string(",")
+    |> ignore(string(","))
     |> concat(allow_space)
     |> parsec(:type)
     |> concat(allow_space)
-    |> string(")")
+    |> ignore(string(")"))
     |> label("map type")
+    |> Helper.to_ast(:map_type)
 
   tuple_type =
     ignore(string("{"))
@@ -105,6 +103,11 @@ defmodule Fika.Parser.Types do
     |> label("nothing")
     |> reduce({Helper, :to_atom, []})
 
+  bool_type =
+    string("Bool")
+    |> label("boolean")
+    |> reduce({Helper, :to_atom, []})
+
   base_type =
     choice([
       string_type,
@@ -112,6 +115,7 @@ defmodule Fika.Parser.Types do
       float_type,
       nothing_type,
       atom_type,
+      bool_type,
       function_type,
       list_type,
       record_type,


Test patch:
diff --git a/test/fika/parser_test.exs b/test/fika/parser_test.exs
index 979e413..dbf4fe9 100644
--- a/test/fika/parser_test.exs
+++ b/test/fika/parser_test.exs
@@ -274,19 +274,16 @@ defmodule Fika.ParserTest do
       end
       """
 
-      {: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}
-                  ]}
-               }
-             ]
+      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)
     end
 
     test "+ and - are parsed as binary operators when on the same line of the first operand" do
@@ -297,22 +294,19 @@ defmodule Fika.ParserTest do
       end
       """
 
-      {: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}
-                  ]}
-               }
-             ]
+      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)
     end
 
     test "add/sub has less precedence than mult/div" do
@@ -421,12 +415,9 @@ defmodule Fika.ParserTest do
       end
       """
 
-      {: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}]}}
-             ]
+      assert {:function, [position: {3, 16, 19}],
+              {:foo, [], {:type, {1, 0, 6}, :Nothing}, [{:integer, {2, 10, 15}, 123}]}} ==
+               TestParser.function_def!(str)
     end
 
     test "with return type Int" do
@@ -436,12 +427,9 @@ defmodule Fika.ParserTest do
       end
       """
 
-      {: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}]}}
-             ]
+      assert {:function, [position: {3, 22, 25}],
+              {:foo, [], {:type, {1, 0, 12}, :Int}, [{:integer, {2, 16, 21}, 123}]}} ==
+               TestParser.function_def!(str)
     end
 
     test "with return type :ok" do
@@ -451,12 +439,9 @@ defmodule Fika.ParserTest do
       end
       """
 
-      {: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}]}}
-             ]
+      assert {:function, [position: {3, 22, 25}],
+              {:foo, [], {:type, {1, 0, 12}, :ok}, [{:integer, {2, 16, 21}, 123}]}} ==
+               TestParser.function_def!(str)
     end
 
     test "with type params" do
@@ -466,17 +451,14 @@ defmodule Fika.ParserTest do
       end
       """
 
-      {: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}]}}
-             ]
+      assert {: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}]}} ==
+               TestParser.function_def!(str)
     end
 
     test "with args" do
@@ -486,20 +468,17 @@ defmodule Fika.ParserTest do
       end
       """
 
-      {: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}
-                 ]}}
-             ]
+      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)
     end
 
     test "with union types" do
@@ -739,10 +718,8 @@ defmodule Fika.ParserTest do
       {"a" => 1}
       """
 
-      result = TestParser.expression!(str)
-
-      assert result ==
-               {:map, {1, 0, 10}, [{{:string, {1, 0, 4}, ["a"]}, {:integer, {1, 0, 9}, 1}}]}
+      assert {:map, {1, 0, 10}, [{{:string, {1, 0, 4}, ["a"]}, {:integer, {1, 0, 9}, 1}}]} ==
+               TestParser.expression!(str)
     end
 
     test "parses map with complex expression" do
@@ -750,47 +727,41 @@ defmodule Fika.ParserTest do
       {[1+1, 2] => {"1" => {1, 2, 3}}}
       """
 
-      result = TestParser.expression!(str)
-
-      assert result ==
-               {
-                 :map,
-                 {1, 0, 32},
-                 [
-                   {
-                     {:list, {1, 0, 9},
-                      [
-                        {:call, {:+, {1, 0, 5}},
-                         [{:integer, {1, 0, 3}, 1}, {:integer, {1, 0, 5}, 1}], :kernel},
-                        {:integer, {1, 0, 8}, 2}
-                      ]},
-                     {:map, {1, 0, 31},
-                      [
-                        {{:string, {1, 0, 17}, ["1"]},
-                         {:tuple, {1, 0, 30},
-                          [
-                            {:integer, {1, 0, 23}, 1},
-                            {:integer, {1, 0, 26}, 2},
-                            {:integer, {1, 0, 29}, 3}
-                          ]}}
-                      ]}
-                   }
-                 ]
-               }
+      assert {
+               :map,
+               {1, 0, 32},
+               [
+                 {
+                   {:list, {1, 0, 9},
+                    [
+                      {:call, {:+, {1, 0, 5}},
+                       [{:integer, {1, 0, 3}, 1}, {:integer, {1, 0, 5}, 1}], :kernel},
+                      {:integer, {1, 0, 8}, 2}
+                    ]},
+                   {:map, {1, 0, 31},
+                    [
+                      {{:string, {1, 0, 17}, ["1"]},
+                       {:tuple, {1, 0, 30},
+                        [
+                          {:integer, {1, 0, 23}, 1},
+                          {:integer, {1, 0, 26}, 2},
+                          {:integer, {1, 0, 29}, 3}
+                        ]}}
+                    ]}
+                 }
+               ]
+             } == TestParser.expression!(str)
 
       str = """
       {true & false => false}
       """
 
-      result = TestParser.expression!(str)
-
-      assert result ==
-               {:map, {1, 0, 23},
-                [
-                  {{:call, {:&, {1, 0, 13}},
-                    [{:boolean, {1, 0, 5}, true}, {:boolean, {1, 0, 13}, false}], :kernel},
-                   {:boolean, {1, 0, 22}, false}}
-                ]}
+      assert {:map, {1, 0, 23},
+              [
+                {{:call, {:&, {1, 0, 13}},
+                  [{:boolean, {1, 0, 5}, true}, {:boolean, {1, 0, 13}, false}], :kernel},
+                 {:boolean, {1, 0, 22}, false}}
+              ]} == TestParser.expression!(str)
     end
 
     test "parses map with function as key-values" do
@@ -798,33 +769,26 @@ defmodule Fika.ParserTest do
       {foo(1, 2) => bar(true)}
       """
 
-      result = TestParser.expression!(str)
-
-      assert result ==
-               {:map, {1, 0, 24},
-                [
-                  {{:call, {:foo, {1, 0, 10}},
-                    [{:integer, {1, 0, 6}, 1}, {:integer, {1, 0, 9}, 2}], nil},
-                   {:call, {:bar, {1, 0, 23}}, [{:boolean, {1, 0, 22}, true}], nil}}
-                ]}
+      assert {:map, {1, 0, 24},
+              [
+                {{:call, {:foo, {1, 0, 10}}, [{:integer, {1, 0, 6}, 1}, {:integer, {1, 0, 9}, 2}],
+                  nil}, {:call, {:bar, {1, 0, 23}}, [{:boolean, {1, 0, 22}, true}], nil}}
+              ]} == TestParser.expression!(str)
 
       str = """
       {&foo.bar => jar(true)}
       """
 
-      result = TestParser.expression!(str)
-
-      assert result ==
-               {
-                 :map,
-                 {1, 0, 23},
-                 [
-                   {
-                     {:function_ref, {1, 0, 9}, {:foo, :bar, []}},
-                     {:call, {:jar, {1, 0, 22}}, [{:boolean, {1, 0, 21}, true}], nil}
-                   }
-                 ]
-               }
+      assert {
+               :map,
+               {1, 0, 23},
+               [
+                 {
+                   {:function_ref, {1, 0, 9}, {:foo, :bar, []}},
+                   {:call, {:jar, {1, 0, 22}}, [{:boolean, {1, 0, 21}, true}], nil}
+                 }
+               ]
+             } == TestParser.expression!(str)
     end
   end
 
@@ -868,73 +832,76 @@ defmodule Fika.ParserTest do
     test "types with no args" do
       str = "Int"
 
-      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.type_str(str)
+      assert {:type, {1, 0, 3}, :Int} == TestParser.type_str!(str)
+
+      str = "Bool"
 
-      assert result == [{:type, {1, 0, 3}, :Int}]
+      assert {:type, {1, 0, 4}, :Bool} == TestParser.type_str!(str)
     end
 
     test "parses types with an arg" do
       str = "List(Int)"
 
-      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.type_str(str)
+      assert {:type, {1, 0, 9}, %T.List{type: :Int}} == TestParser.type_str!(str)
 
-      assert result == [{:type, {1, 0, 9}, %T.List{type: :Int}}]
+      str = "Map(Int, String)"
+
+      assert {:type, {1, 0, 16}, %T.Map{key_type: :Int, value_type: :String}} ==
+               TestParser.type_str!(str)
     end
 
     test "parses types with nested args" do
       str = "List(List(String))"
 
-      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.type_str(str)
+      assert {:type, {1, 0, 18}, %T.List{type: %T.List{type: :String}}} ==
+               TestParser.type_str!(str)
+
+      str = "Map(Int, Map(String, Bool))"
 
-      assert result == [{:type, {1, 0, 18}, %T.List{type: %T.List{type: :String}}}]
+      assert {:type, {1, 0, 27},
+              %T.Map{
+                key_type: :Int,
+                value_type: %T.Map{key_type: :String, value_type: :Bool}
+              }} == TestParser.type_str!(str)
     end
 
     test "parses function type with no args" do
       str = "Fn(-> Int)"
 
-      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.type_str(str)
-
-      assert result == [
-               {:type, {1, 0, 10},
-                %T.FunctionRef{
-                  arg_types: [],
-                  return_type: :Int
-                }}
-             ]
+      assert {:type, {1, 0, 10},
+              %T.FunctionRef{
+                arg_types: [],
+                return_type: :Int
+              }} == TestParser.type_str!(str)
     end
 
     test "parses function type with args" do
       str = "Fn(Int, Int -> Int)"
 
-      {: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
-                }}
-             ]
+      assert {:type, {1, 0, 19},
+              %T.FunctionRef{
+                arg_types: [:Int, :Int],
+                return_type: :Int
+              }} == TestParser.type_str!(str)
     end
 
     test "record type" do
       str = "{foo: Int, bar: String}"
 
-      assert {:type, {1, 0, 23}, "{foo:Int,bar:String}"} == TestParser.type_str!(str)
+      assert {:type, {1, 0, 23}, %T.Record{fields: [foo: :Int, bar: :String]}} ==
+               TestParser.type_str!(str)
     end
 
     test "atom type" do
       str = ":foo"
 
-      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.type_str(str)
-      assert result == [{:type, {1, 0, 4}, :foo}]
+      assert {:type, {1, 0, 4}, :foo} == TestParser.type_str!(str)
     end
 
     test "list of atom" do
       str = "List(:foo)"
 
-      {:ok, result, _rest, _context, _line, _byte_offset} = TestParser.type_str(str)
-      assert result == [{:type, {1, 0, 10}, %T.List{type: :foo}}]
+      assert {:type, {1, 0, 10}, %T.List{type: :foo}} == TestParser.type_str!(str)
     end
   end
 
@@ -944,10 +911,7 @@ defmodule Fika.ParserTest do
       &foo
       """
 
-      result = TestParser.expression!(str)
-
-      assert result ==
-               {:function_ref, {1, 0, 4}, {nil, :foo, []}}
+      assert {:function_ref, {1, 0, 4}, {nil, :foo, []}} == TestParser.expression!(str)
     end
 
     test "parses a remote function ref with no args" do
@@ -955,10 +919,7 @@ defmodule Fika.ParserTest do
       &foo.bar
       """
 
-      result = TestParser.expression!(str)
-
-      assert result ==
-               {:function_ref, {1, 0, 8}, {:foo, :bar, []}}
+      assert {:function_ref, {1, 0, 8}, {:foo, :bar, []}} == TestParser.expression!(str)
     end
 
     test "parses a function ref with arg types" do
@@ -966,10 +927,8 @@ defmodule Fika.ParserTest do
       &foo.bar(Int, Int)
       """
 
-      result = TestParser.expression!(str)
-
-      assert result ==
-               {:function_ref, {1, 0, 18}, {:foo, :bar, [:Int, :Int]}}
+      assert {:function_ref, {1, 0, 18}, {:foo, :bar, [:Int, :Int]}} ==
+               TestParser.expression!(str)
     end
   end
 
@@ -982,12 +941,9 @@ defmodule Fika.ParserTest do
       end
       """
 
-      {: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}]}}
-             ]
+      assert {:function, [position: {4, 36, 39}],
+              {:foo, [], {:type, {2, 20, 26}, :Nothing}, [{:integer, {3, 30, 35}, 123}]}} ==
+               TestParser.function_def!(str)
     end
 
     test "At end of line" do
@@ -997,12 +953,9 @@ defmodule Fika.ParserTest do
       end # Comment 3
       """
 
-      {: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}]}}
-             ]
+      assert {:function, [position: {3, 40, 43}],
+              {:foo, [], {:type, {1, 0, 6}, :Nothing}, [{:integer, {2, 22, 27}, 123}]}} ==
+               TestParser.function_def!(str)
     end
 
     test "Can't appear in between characters" do
@@ -1022,12 +975,9 @@ defmodule Fika.ParserTest do
       end
       """
 
-      {: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"]}]}}
-             ]
+      assert {:function, [position: {3, 22, 25}],
+              {:foo, [], {:type, {1, 0, 6}, :Nothing}, [{:string, {2, 10, 21}, ["foo#bar"]}]}} ==
+               TestParser.function_def!(str)
     end
   end