Repository: fika-lang/fika
Pull request #84 (2021-02-12 00:15:59)

Add map, reduce, filter, length functions under list stdlib module


Patch:
diff --git a/config/config.exs b/config/config.exs
index a32420e..890373e 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -4,6 +4,4 @@ unless System.get_env("BAKEWARE_ARGC") do
   System.put_env("BAKEWARE_ARGC", "0")
 end
 
-config :fika,
-  router_path: "examples/router.fi",
-  remote_endpoint: "https://fikaapp.com/code"
+import_config "#{Mix.env()}.exs"
diff --git a/config/dev.exs b/config/dev.exs
new file mode 100644
index 0000000..9764884
--- /dev/null
+++ b/config/dev.exs
@@ -0,0 +1,4 @@
+import Config
+
+config :fika,
+  router_path: "examples/router.fi"
diff --git a/config/prod.exs b/config/prod.exs
new file mode 100644
index 0000000..9f330e0
--- /dev/null
+++ b/config/prod.exs
@@ -0,0 +1,4 @@
+import Config
+
+config :fika,
+  router_path: "router.fi"
diff --git a/fika/io.fi b/fika/io.fi
index ba5836f..4859a55 100644
--- a/fika/io.fi
+++ b/fika/io.fi
@@ -1,2 +1,2 @@
 ext gets(prompt: String) : Effect(String) = {"Elixir.IO", "gets", [prompt]}
-ext puts(str: String) : Effect(:ok) = {"Elixir.IO", "puts", [str]}
+ext puts(str: String) : Effect(String) = {"Elixir.IO", "puts", [str]}
diff --git a/fika/list.fi b/fika/list.fi
new file mode 100644
index 0000000..0029ac5
--- /dev/null
+++ b/fika/list.fi
@@ -0,0 +1,5 @@
+ext map(list: List(a), fun: Fn(a->b)) : b = {"Elixir.Enum", "map", [list, fun]}
+ext length(list: List(a)) : Int = {"Elixir.Enum", "count", [list]}
+ext filter(list: List(a), fun: Fn(a->Bool)) : List(a) = {"Elixir.Enum", "filter", [list, fun]}
+ext reduce(list: List(a), fun: Fn(a,b->b)) : b = {"Elixir.Enum", "reduce", [list, fun]}
+ext reduce(list: List(a), acc: b, fun: Fn(a,b->b)) : b = {"Elixir.Enum", "reduce", [list, acc, fun]}
diff --git a/lib/fika/compiler/code_server.ex b/lib/fika/compiler/code_server.ex
index 1718bf6..5f6d1ca 100644
--- a/lib/fika/compiler/code_server.ex
+++ b/lib/fika/compiler/code_server.ex
@@ -34,6 +34,10 @@ defmodule Fika.Compiler.CodeServer do
     GenServer.call(__MODULE__, {:get_type, signature})
   end
 
+  def list_all_types do
+    GenServer.call(__MODULE__, :list_all_types)
+  end
+
   def set_type(signature, result) do
     GenServer.cast(__MODULE__, {:set_type, signature, result})
   end
@@ -146,6 +150,10 @@ defmodule Fika.Compiler.CodeServer do
     {:noreply, state}
   end
 
+  def handle_call(:list_all_types, _from, state) do
+    {:reply, state.public_functions, state}
+  end
+
   def handle_call({:compile_module, module}, from, _state) do
     state =
       init_state()
@@ -271,11 +279,12 @@ defmodule Fika.Compiler.CodeServer do
       compile_result: %{},
       parent_pid: nil,
       dev_token: Application.get_env(:fika, :dev_token),
-      remote_endpoint: Application.get_env(:fika, :remote_endpoint),
+      remote_endpoint: Application.get_env(:fika, :remote_endpoint, "https://fikaapp.com/code"),
       binaries: []
     }
     |> put_default_types(DefaultTypes.kernel())
     |> put_default_types(DefaultTypes.io())
+    |> put_default_types(DefaultTypes.list())
   end
 
   defp reset_binaries(state) do
diff --git a/lib/fika/compiler/default_types.ex b/lib/fika/compiler/default_types.ex
index 03d7a36..f64392d 100644
--- a/lib/fika/compiler/default_types.ex
+++ b/lib/fika/compiler/default_types.ex
@@ -40,6 +40,9 @@ defmodule Fika.Compiler.DefaultTypes do
     ]
   end
 
+  # TODO: currently, the type signatures of stdlib functions are set
+  # using the functions below. In the future, we should load these type
+  # signatures automatically.
   def io do
     m = "fika/io"
 
@@ -49,6 +52,63 @@ defmodule Fika.Compiler.DefaultTypes do
     ]
   end
 
+  def list do
+    m = "fika/list"
+
+    [
+      s(
+        m,
+        "map",
+        [
+          %T.List{type: "a"},
+          %T.FunctionRef{
+            return_type: "b",
+            arg_types: ["a"]
+          }
+        ],
+        %T.List{type: "b"}
+      ),
+      s(m, "length", [%T.List{type: "a"}], :Int),
+      s(
+        m,
+        "filter",
+        [
+          %T.List{type: "a"},
+          %T.FunctionRef{
+            return_type: :Bool,
+            arg_types: ["a"]
+          }
+        ],
+        %T.List{type: "a"}
+      ),
+      s(
+        m,
+        "reduce",
+        [
+          %T.List{type: "a"},
+          %T.FunctionRef{
+            return_type: "b",
+            arg_types: ["a", "b"]
+          }
+        ],
+        "b"
+      ),
+      s(
+        m,
+        "reduce",
+        [
+          %T.List{type: "a"},
+          "b",
+          %T.FunctionRef{
+            arg_types: ["a", "b"],
+            return_type: "b"
+          }
+        ],
+        "b"
+      )
+    ]
+  end
+
   # Helper function to create signature structs out of module, function,
   # arg types and return type.
   defp s(m, f, t, r) do
diff --git a/lib/fika/compiler/parser/helper.ex b/lib/fika/compiler/parser/helper.ex
index 5eb68f0..0fc5913 100644
--- a/lib/fika/compiler/parser/helper.ex
+++ b/lib/fika/compiler/parser/helper.ex
@@ -279,11 +279,13 @@ defmodule Fika.Compiler.Parser.Helper do
   end
 
   defp expand_module({:module_name, line, module}, context) do
+    fika_modules = ["kernel", "io", "list"]
+
     module =
-      case module do
-        "kernel" -> "fika/kernel"
-        "io" -> "fika/io"
-        _ -> context[module]
+      if module in fika_modules do
+        "fika/#{module}"
+      else
+        context[module]
       end
 
     module && {:identifier, line, module}
diff --git a/lib/fika/compiler/type_checker/function_match.ex b/lib/fika/compiler/type_checker/function_match.ex
index b2ab7a3..4dcf166 100644
--- a/lib/fika/compiler/type_checker/function_match.ex
+++ b/lib/fika/compiler/type_checker/function_match.ex
@@ -5,8 +5,6 @@ defmodule Fika.Compiler.TypeChecker.FunctionMatch do
   # finds the value for the signature which is a supertype of the second arg.
   def find_by_call(map, signature) when is_map(map) do
     Enum.find_value(map, fn {s, v} ->
-      # signature_matches_call?(s, signature)
-
       if vars = match_signatures(s, signature) do
         {s, v, vars}
       end
@@ -31,7 +29,7 @@ defmodule Fika.Compiler.TypeChecker.FunctionMatch do
   end
 
   defp do_replace_vars(var, vars) when is_binary(var) do
-    vars[var]
+    vars[var] || var
   end
 
   defp do_replace_vars(%T.Union{types: ts}, vars) do
@@ -91,13 +89,11 @@ defmodule Fika.Compiler.TypeChecker.FunctionMatch do
          %T.FunctionRef{arg_types: a_t2s, return_type: r_t2},
          vars
        ) do
-    if new_vars = match_all_subtypes(a_t2s, a_t1s, %{}) do
-      r_t2 = do_replace_vars(r_t2, new_vars)
+    if new_vars = match_all_subtypes(a_t1s, a_t2s, vars) do
+      r_t1 = do_replace_vars(r_t1, new_vars)
 
       if new_vars = do_match_subtype(r_t1, r_t2, new_vars) do
-        new_vars
-        |> invert_bindings()
-        |> merge_vars(vars)
+        merge_vars(new_vars, vars)
       end
     end
   end
@@ -119,6 +115,10 @@ defmodule Fika.Compiler.TypeChecker.FunctionMatch do
     do_match_all_subtype(t1s, t2s, vars)
   end
 
+  defp do_match_subtype(%T.List{type: t1}, %T.List{type: t2}, vars) do
+    do_match_subtype(t1, t2, vars)
+  end
+
   defp do_match_subtype(_, _, _) do
     nil
   end
@@ -134,10 +134,6 @@ defmodule Fika.Compiler.TypeChecker.FunctionMatch do
     end
   end
 
-  defp invert_bindings(vars) do
-    for {k, v} <- vars, into: %{}, do: {v, k}
-  end
-
   defp merge_vars(arg_vars, vars) do
     Enum.reduce_while(arg_vars, vars, fn {k, v}, acc ->
       case acc[k] do
diff --git a/lib/fika/router.ex b/lib/fika/router.ex
index f4cee12..98461cc 100644
--- a/lib/fika/router.ex
+++ b/lib/fika/router.ex
@@ -10,7 +10,7 @@ defmodule Fika.Router do
     send_resp(conn, status, resp)
   end
 
-  def create_example_router do
+  def create_example_router(path) do
     content = """
     fn routes : List({method: String, path: String, handler: Fn(->String)}) do
       [
@@ -23,7 +23,7 @@ defmodule Fika.Router do
     end
     """
 
-    File.write("router.fi", content)
+    File.write(path, content)
   end
 
   defp get_resp(method, path) do
diff --git a/lib/fika/router/supervisor.ex b/lib/fika/router/supervisor.ex
index 5788de5..6a46c43 100644
--- a/lib/fika/router/supervisor.ex
+++ b/lib/fika/router/supervisor.ex
@@ -15,9 +15,11 @@ defmodule Fika.Router.Supervisor do
 
   @impl true
   def init(_) do
-    unless File.exists?("router.fi") do
-      IO.puts("router.fi not found. Creating an example router in file router.fi")
-      Fika.Router.create_example_router()
+    router = Application.get_env(:fika, :router_path)
+
+    unless File.exists?(router) do
+      IO.puts("Router not found. Creating an example router in file #{router}")
+      Fika.Router.create_example_router(router)
     end
 
     children = [
diff --git a/lib/mix/tasks/compile.fika.ex b/lib/mix/tasks/compile.fika.ex
index 68ec763..dd408ea 100644
--- a/lib/mix/tasks/compile.fika.ex
+++ b/lib/mix/tasks/compile.fika.ex
@@ -10,16 +10,20 @@ defmodule Mix.Tasks.Compile.Fika do
     {:ok, _} = Application.ensure_all_started(:fika)
 
     dest = Mix.Project.compile_path()
-    default_modules = ["fika/io"]
 
-    Enum.each(default_modules, fn module ->
-      Logger.info("Compiling #{module}")
-      Code.compile_to_path(module, dest)
-    end)
+    "fika/**/*.fi"
+    |> Path.wildcard()
+    |> Enum.each(&compile_to_path(&1, dest))
 
     Application.stop(:fika)
     Logger.configure(level: level)
 
     :ok
   end
+
+  defp compile_to_path(file, dest) do
+    module = String.replace_suffix(file, ".fi", "")
+    Logger.info("Compiling stdlib module: #{module}")
+    Code.compile_to_path(module, dest)
+  end
 end


Test patch:
diff --git a/config/test.exs b/config/test.exs
new file mode 100644
index 0000000..29ad4ac
--- /dev/null
+++ b/config/test.exs
@@ -0,0 +1,4 @@
+import Config
+
+config :fika,
+  disable_web_server: true
diff --git a/test/fika/compiler/type_checker/function_match_test.exs b/test/fika/compiler/type_checker/function_match_test.exs
index 11f404a..e17b2db 100644
--- a/test/fika/compiler/type_checker/function_match_test.exs
+++ b/test/fika/compiler/type_checker/function_match_test.exs
@@ -62,6 +62,54 @@ defmodule Fika.Compiler.TypeChecker.FunctionMatchTest do
     assert FunctionMatch.match_signatures(s1, s2) == %{"a" => :a}
   end
 
+  # This tests function matching of list.map/2
+  test "match_signatures with function refs and type variables - list.map" do
+    t1 = [
+      %T.List{type: "a"},
+      %T.FunctionRef{
+        return_type: "b",
+        arg_types: ["a"]
+      }
+    ]
+
+    t2 = [
+      %T.List{type: :Int},
+      %T.FunctionRef{
+        return_type: :String,
+        arg_types: [:Int]
+      }
+    ]
+
+    s1 = signature("foo", "map", t1)
+    s2 = signature("foo", "map", t2)
+    assert FunctionMatch.match_signatures(s1, s2) == %{"a" => :Int, "b" => :String}
+  end
+
+  # This tests function matching of list.reduce/3
+  test "match_signatures with function refs and type variables - list.reduce" do
+    t1 = [
+      %T.List{type: "a"},
+      "b",
+      %T.FunctionRef{
+        return_type: "b",
+        arg_types: ["a", "b"]
+      }
+    ]
+
+    t2 = [
+      %T.List{type: :Int},
+      :Int,
+      %T.FunctionRef{
+        return_type: :Int,
+        arg_types: [:Int, :Int]
+      }
+    ]
+
+    s1 = signature("foo", "reduce", t1)
+    s2 = signature("foo", "reduce", t2)
+    assert FunctionMatch.match_signatures(s1, s2) == %{"a" => :Int, "b" => :Int}
+  end
+
   test "replace_vars" do
     result = {:ok, :Int}
     assert FunctionMatch.replace_vars(result, %{"a" => :String}) == {:ok, :Int}
diff --git a/test/fika/stdlib/list_test.exs b/test/fika/stdlib/list_test.exs
new file mode 100644
index 0000000..8c4eab0
--- /dev/null
+++ b/test/fika/stdlib/list_test.exs
@@ -0,0 +1,73 @@
+defmodule Fika.Stdlib.ListTest do
+  use ExUnit.Case, async: false
+
+  alias Fika.Code
+
+  test "map" do
+    str = """
+    fn add_one(x: List(Int)) : List(Int) do
+      list.map(x, (y : Int) do
+        y + 1
+      end)
+    end
+    """
+
+    Code.load_file("foo", str)
+
+    assert apply(:foo, :add_one, [[1, 2, 3]]) == [2, 3, 4]
+  end
+
+  test "length" do
+    str = """
+    fn count(x: List(Int)) : Int do
+      list.length(x)
+    end
+    """
+
+    Code.load_file("foo", str)
+
+    assert apply(:foo, :count, [[1, 2, 3]]) == 3
+  end
+
+  test "filter" do
+    str = """
+    fn filter_nums(x: List(Int)) : List(Int) do
+      list.filter(x, (y: Int) do
+        y == 3 | y == 5
+      end)
+    end
+    """
+
+    Code.load_file("foo", str)
+
+    assert apply(:foo, :filter_nums, [[1, 2, 3, 4, 5]]) == [3, 5]
+  end
+
+  test "reduce/3" do
+    str = """
+    fn sum(x: List(Int)) : Int do
+      list.reduce(x, 0, (y: Int, acc: Int) do
+        y + acc
+      end)
+    end
+    """
+
+    Code.load_file("foo", str)
+
+    assert apply(:foo, :sum, [[1, 2, 3, 4, 5]]) == 15
+  end
+
+  test "reduce/2" do
+    str = """
+    fn sum(x: List(Int)) : Int do
+      list.reduce(x, (y: Int, acc: Int) do
+        y + acc
+      end)
+    end
+    """
+
+    Code.load_file("foo", str)
+
+    assert apply(:foo, :sum, [[1, 2, 3, 4, 5]]) == 15
+  end
+end