Repository: fika-lang/fika
Pull request #58 (2020-12-01 07:55:54)

Add anonymous functions


Patch:
diff --git a/example.fi b/example.fi
index 805ffc0..54b1862 100644
--- a/example.fi
+++ b/example.fi
@@ -10,6 +10,15 @@ fn hello : String do
   "World"
 end
 
+# Anonymous function
+fn anonymous_call : Int do
+  f = (x: Int, y: Int) do
+    x + y
+  end
+
+  f.(10, 20)
+end
+
 # Strings support interpolations
 fn hello_interpolation : String do
   hello = "Hello!"
diff --git a/lib/fika/compiler/erl_translate.ex b/lib/fika/compiler/erl_translate.ex
index 98e5e0e..67a1af5 100644
--- a/lib/fika/compiler/erl_translate.ex
+++ b/lib/fika/compiler/erl_translate.ex
@@ -191,6 +191,10 @@ defmodule Fika.Compiler.ErlTranslate do
     }
   end
 
+  defp translate_exp({:anonymous_function, {line, _, _}, args, exps}) do
+    {:fun, line, {:clauses, [translate_clauses(args, line, exps)]}}
+  end
+
   defp add_record_meta(k_vs, name, line) do
     name =
       if name do
diff --git a/lib/fika/compiler/parser/function_def.ex b/lib/fika/compiler/parser/function_def.ex
index fd02244..38c9493 100644
--- a/lib/fika/compiler/parser/function_def.ex
+++ b/lib/fika/compiler/parser/function_def.ex
@@ -127,7 +127,7 @@ defmodule Fika.Compiler.Parser.FunctionDef do
     function_def
     |> times(min: 1)
 
-  defcombinatorp :args, args
+  defcombinator :args, args
   defcombinator :function_def, function_def
   defcombinator :function_defs, function_defs
 end
diff --git a/lib/fika/compiler/parser/helper.ex b/lib/fika/compiler/parser/helper.ex
index 3ad0121..e0c11ed 100644
--- a/lib/fika/compiler/parser/helper.ex
+++ b/lib/fika/compiler/parser/helper.ex
@@ -54,6 +54,10 @@ defmodule Fika.Compiler.Parser.Helper do
     {{:if, line}, condition, true_block, false_block}
   end
 
+  def do_to_ast({[args, exps], line}, :anonymous_function) do
+    {:anonymous_function, line, args, exps}
+  end
+
   def do_to_ast({[left, bin_op, right | rest], line}, :exp_bin_op)
       when bin_op in ["+", "-", "*", "/", "|", "&", "<", ">", "<=", ">=", "==", "!="] do
     new_left = {:call, {String.to_atom(bin_op), line}, [left, right], "fika/kernel"}
diff --git a/lib/fika/compiler/parser/non_literal_exps.ex b/lib/fika/compiler/parser/non_literal_exps.ex
index 694216f..93a863e 100644
--- a/lib/fika/compiler/parser/non_literal_exps.ex
+++ b/lib/fika/compiler/parser/non_literal_exps.ex
@@ -1,7 +1,7 @@
 defmodule Fika.Compiler.Parser.NonLiteralExps do
   import NimbleParsec
 
-  alias Fika.Compiler.Parser.{Common, Helper, Expressions}
+  alias Fika.Compiler.Parser.{Common, Helper, Expressions, FunctionDef}
 
   allow_space = parsec({Common, :allow_space})
   require_space = parsec({Common, :require_space})
@@ -9,6 +9,7 @@ defmodule Fika.Compiler.Parser.NonLiteralExps do
   module_name = parsec({Common, :module_name})
   exp = parsec({Expressions, :exp})
   exps = parsec({Expressions, :exps})
+  args = parsec({FunctionDef, :args})
 
   exp_paren =
     ignore(string("("))
@@ -50,6 +51,24 @@ defmodule Fika.Compiler.Parser.NonLiteralExps do
     ])
     |> label("function call")
 
+  arg_parens =
+    ignore(string("("))
+    |> concat(allow_space)
+    |> wrap(optional(args))
+    |> concat(allow_space)
+    |> ignore(string(")"))
+
+  anonymous_function =
+    arg_parens
+    |> concat(allow_space)
+    |> ignore(string("do"))
+    |> concat(allow_space)
+    |> wrap(exps)
+    |> concat(allow_space)
+    |> ignore(string("end"))
+    |> label("anonymous function")
+    |> Helper.to_ast(:anonymous_function)
+
   # TO-DO: accept nested if-else expressions
   exp_if_else =
     ignore(string("if"))
@@ -79,7 +98,8 @@ defmodule Fika.Compiler.Parser.NonLiteralExps do
       exp_paren,
       function_call,
       identifier,
-      exp_if_else
+      exp_if_else,
+      anonymous_function
     ])
     |> optional(function_ref_call)
     |> Helper.to_ast(:function_ref_call)
diff --git a/lib/fika/compiler/type_checker.ex b/lib/fika/compiler/type_checker.ex
index ba30279..71e104d 100644
--- a/lib/fika/compiler/type_checker.ex
+++ b/lib/fika/compiler/type_checker.ex
@@ -264,6 +264,26 @@ defmodule Fika.Compiler.TypeChecker do
     end
   end
 
+  # anonymous function
+  def infer_exp(env, {:anonymous_function, _line, args, exps}) do
+    Logger.debug("Inferring type of anonymous function")
+
+    env =
+      env
+      |> Map.put(:scope, %{})
+      |> add_args_to_scope(args)
+
+    case infer_block(env, exps) do
+      {:ok, return_type, _env} ->
+        arg_types = Enum.map(args, fn {_, {:type, _, type}} -> type end)
+        type = %T.FunctionRef{arg_types: arg_types, return_type: return_type}
+        {:ok, type, env}
+
+      error ->
+        error
+    end
+  end
+
   def function_ast_signature({:function, _line, {name, args, _type, _exprs}}) do
     arg_types = Enum.map(args, fn {_, {:type, _, type}} -> type end)
     get_function_signature(name, arg_types)


Test patch:
diff --git a/test/fika/compiler/erl_translate_test.exs b/test/fika/compiler/erl_translate_test.exs
index 008a07c..18bd6dc 100644
--- a/test/fika/compiler/erl_translate_test.exs
+++ b/test/fika/compiler/erl_translate_test.exs
@@ -285,4 +285,18 @@ defmodule Fika.Compiler.ErlTranslateTest do
 
     assert result == forms
   end
+
+  test "anonymous function" do
+    str = "(x: Int) do x + 10 end"
+    ast = TestParser.expression!(str)
+    result = ErlTranslate.translate_expression(ast)
+
+    assert result ==
+             {:fun, 1,
+              {:clauses,
+               [
+                 {:clause, 1, [{:var, 1, :x}], [],
+                  [{:op, 1, :+, {:var, 1, :x}, {:integer, 1, 10}}]}
+               ]}}
+  end
 end
diff --git a/test/fika/compiler/parser_test.exs b/test/fika/compiler/parser_test.exs
index f31b545..53b7399 100644
--- a/test/fika/compiler/parser_test.exs
+++ b/test/fika/compiler/parser_test.exs
@@ -415,6 +415,27 @@ defmodule Fika.Compiler.ParserTest do
     end
   end
 
+  describe "anonymous function" do
+    test "with args" do
+      str = """
+      (x: Int) do x end
+      """
+
+      assert {:anonymous_function, {1, 0, 17},
+              [{{:identifier, {1, 0, 2}, :x}, {:type, {1, 0, 7}, :Int}}],
+              [{:identifier, {1, 0, 13}, :x}]} = TestParser.expression!(str)
+    end
+
+    test "without args" do
+      str = """
+      () do 123 end
+      """
+
+      assert {:anonymous_function, {1, 0, 13}, [], [{:integer, {1, 0, 9}, 123}]} =
+               TestParser.expression!(str)
+    end
+  end
+
   describe "match expression" do
     test "simple match" do
       str = """
diff --git a/test/fika/compiler/type_checker_test.exs b/test/fika/compiler/type_checker_test.exs
index d50c5dc..72ca346 100644
--- a/test/fika/compiler/type_checker_test.exs
+++ b/test/fika/compiler/type_checker_test.exs
@@ -624,4 +624,22 @@ defmodule Fika.Compiler.TypeCheckerTest do
       assert {:error, ^error} = TypeChecker.infer(function, %{})
     end
   end
+
+  describe "anonymous function" do
+    test "with arg types" do
+      str = """
+      (x: Int, y: Int) do
+        x + y
+      end
+      """
+
+      ast = TestParser.expression!(str)
+
+      assert {:ok,
+              %T.FunctionRef{
+                arg_types: [:Int, :Int],
+                return_type: :Int
+              }, _} = TypeChecker.infer_exp(%{}, ast)
+    end
+  end
 end