Repository: fika-lang/fika
Pull request #52 (2020-11-24 16:01:03)

Call external functions by wrapping them in Fika function signatures
This allows developers to call external functions which are defined in elixir, erlang etc by creating a function signature for them in Fika using a one-liner syntax. This is unsafe, and only to be used when developers know for sure the return types of the external functions because the Fika compiler has no way to verify them. The safe way to call external functions is to use Fika's effect system (coming soon) but this unsafe way allows developers to avoid "coloring" all callers as "effectful" when the called external function is guaranteed to be pure. This will also help us build our stdlib easily by calling into Elixir/Erlang stdlib functions instead of reinventing the wheel.


Patch:
diff --git a/README.md b/README.md
index 2ad61b3..dbc2ab4 100644
--- a/README.md
+++ b/README.md
@@ -104,6 +104,22 @@ record = {foo: 123}
 [example.fi](https://github.com/fika-lang/fika/blob/main/example.fi) has
 working examples that demonstrate the syntax.
 
+
+#### Interop with Elixir and Erlang
+
+Fika makes it easy to call functions defined externally in the BEAM:
+
+```
+# Inside module foo
+ext str_length(str: String) : Int = {"Elixir.String", "length", [str]}
+
+# Can be called using `foo.str_length("Hello world")`
+```
+
+When reaching out to functions external to Fika, the compiler
+blindly trusts the type signature provided by the developer, so be careful here!
+
+
 ### Running Fika programs
 
 Fika is written in Elixir, so make sure you have that installed.
diff --git a/example.fi b/example.fi
index 1c2dfab..805ffc0 100644
--- a/example.fi
+++ b/example.fi
@@ -104,3 +104,15 @@ fn interval(start: Int, finish: Int) : {:error, String} | {:ok, Int} do
     {:error, "Interval cannot end before it's started"}
   end
 end
+
+# Wrap and use external functions defined in the BEAM
+ext str_length(str: String) : Int = {"Elixir.String", "length", [str]}
+ext list_size(l: List(Int)) : Int = {"erlang", "length", [l]}
+
+fn string_length : Int do
+  str_length("Hello world")
+end
+
+fn list_length : Int do
+  list_size([1, 2, 3])
+end
diff --git a/lib/fika/compiler/erl_translate.ex b/lib/fika/compiler/erl_translate.ex
index 95e76d4..98e5e0e 100644
--- a/lib/fika/compiler/erl_translate.ex
+++ b/lib/fika/compiler/erl_translate.ex
@@ -87,6 +87,11 @@ defmodule Fika.Compiler.ErlTranslate do
     {:call, line, translate_exp(identifier), translate_exps(args)}
   end
 
+  defp translate_exp({:ext_call, {line, _, _}, {m, f, args, _}}) do
+    m_f = {:remote, line, {:atom, line, m}, {:atom, line, f}}
+    {:call, line, m_f, translate_exps(args)}
+  end
+
   defp translate_exp({:integer, {line, _, _}, value}) do
     {:integer, line, value}
   end
diff --git a/lib/fika/compiler/parser/function_def.ex b/lib/fika/compiler/parser/function_def.ex
index 41519b2..fd02244 100644
--- a/lib/fika/compiler/parser/function_def.ex
+++ b/lib/fika/compiler/parser/function_def.ex
@@ -41,6 +41,22 @@ defmodule Fika.Compiler.Parser.FunctionDef do
       empty() |> wrap()
     ])
 
+  arg_names =
+    identifier
+    |> repeat(
+      allow_space
+      |> ignore(string(","))
+      |> concat(allow_space)
+      |> concat(identifier)
+    )
+
+  arg_list =
+    ignore(string("["))
+    |> concat(allow_space)
+    |> wrap(optional(arg_names))
+    |> concat(allow_space)
+    |> ignore(string("]"))
+
   return_type =
     optional(
       allow_space
@@ -50,7 +66,7 @@ defmodule Fika.Compiler.Parser.FunctionDef do
     )
     |> Helper.to_ast(:return_type)
 
-  function_def =
+  public_function_def =
     allow_space
     |> ignore(string("fn"))
     |> concat(require_space)
@@ -63,12 +79,52 @@ defmodule Fika.Compiler.Parser.FunctionDef do
     |> wrap(exps)
     |> concat(require_space)
     |> ignore(string("end"))
-    |> label("function definition")
-    |> Helper.to_ast(:function_def)
+    |> label("public function definition")
+    |> Helper.to_ast(:public_function_def)
 
-  function_defs =
+  ext_atom =
+    ignore(string("\""))
+    |> repeat(choice([string(~S{\"}), utf8_char(not: ?")]))
+    |> ignore(string("\""))
+    |> reduce({List, :to_atom, []})
+
+  ext_mfa =
+    ignore(string("{"))
+    |> concat(allow_space)
+    |> concat(ext_atom)
+    |> concat(allow_space)
+    |> ignore(string(","))
+    |> concat(allow_space)
+    |> concat(ext_atom)
+    |> concat(allow_space)
+    |> ignore(string(","))
+    |> concat(allow_space)
+    |> concat(arg_list)
+    |> concat(allow_space)
+    |> ignore(string("}"))
+
+  ext_function_def =
     allow_space
-    |> concat(function_def)
+    |> ignore(string("ext"))
+    |> concat(require_space)
+    |> concat(identifier)
+    |> concat(arg_parens)
+    |> concat(return_type)
+    |> concat(allow_space)
+    |> ignore(string("="))
+    |> concat(allow_space)
+    |> concat(ext_mfa)
+    |> label("external function definition")
+    |> Helper.to_ast(:ext_function_def)
+
+  function_def =
+    choice([
+      public_function_def,
+      ext_function_def
+    ])
+
+  function_defs =
+    function_def
     |> times(min: 1)
 
   defcombinatorp :args, args
diff --git a/lib/fika/compiler/parser/helper.ex b/lib/fika/compiler/parser/helper.ex
index 6b675dc..3ad0121 100644
--- a/lib/fika/compiler/parser/helper.ex
+++ b/lib/fika/compiler/parser/helper.ex
@@ -77,11 +77,22 @@ defmodule Fika.Compiler.Parser.Helper do
     {:module_name, line, name}
   end
 
-  def do_to_ast({[name, args, type, exps], line}, :function_def) do
+  def do_to_ast({[name, args, type, exps], line}, :public_function_def) do
     {:identifier, _, name} = name
     {:function, [position: line], {name, args, type, exps}}
   end
 
+  def do_to_ast(
+        {[name, args, type, ext_module, ext_function, arg_names], line},
+        :ext_function_def
+      ) do
+    {:identifier, _, name} = name
+    {:type, _, ext_type} = type
+
+    call = {:ext_call, line, {ext_module, ext_function, arg_names, ext_type}}
+    {:function, [position: line], {name, args, type, [call]}}
+  end
+
   def do_to_ast({[], line}, :return_type) do
     {:type, line, :Nothing}
   end
diff --git a/lib/fika/compiler/parser/literal_exps.ex b/lib/fika/compiler/parser/literal_exps.ex
index af61f69..7dc17e1 100644
--- a/lib/fika/compiler/parser/literal_exps.ex
+++ b/lib/fika/compiler/parser/literal_exps.ex
@@ -30,7 +30,7 @@ defmodule Fika.Compiler.Parser.LiteralExps do
 
   string_exp =
     ignore(string("\""))
-    |> repeat(choice([interpolation, string("\\\""), utf8_char(not: ?")]))
+    |> repeat(choice([interpolation, string(~S{\"}), utf8_char(not: ?")]))
     |> ignore(string("\""))
     |> Helper.to_ast(:string)
 
diff --git a/lib/fika/compiler/type_checker.ex b/lib/fika/compiler/type_checker.ex
index 5cb21e9..ba30279 100644
--- a/lib/fika/compiler/type_checker.ex
+++ b/lib/fika/compiler/type_checker.ex
@@ -89,6 +89,12 @@ defmodule Fika.Compiler.TypeChecker do
     end
   end
 
+  # External function calls
+  def infer_exp(env, {:ext_call, _line, {m, f, _, type}}) do
+    Logger.debug("Return type of ext function #{m}.#{f} specified as #{type}")
+    {:ok, type, env}
+  end
+
   # Function calls
   def infer_exp(env, {:call, {name, _line}, args, module}) do
     exp = %{args: args, name: name}


Test patch:
diff --git a/test/fika/compiler/erl_translate_test.exs b/test/fika/compiler/erl_translate_test.exs
index d28a34c..008a07c 100644
--- a/test/fika/compiler/erl_translate_test.exs
+++ b/test/fika/compiler/erl_translate_test.exs
@@ -256,4 +256,33 @@ defmodule Fika.Compiler.ErlTranslateTest do
                {nil, 1}}} = ErlTranslate.translate_expression(ast)
     end
   end
+
+  test "an ext function call" do
+    str = """
+    ext foo(x: Int) : Int = {"Test", "foo", [x]}
+
+    fn bar do
+      foo(x)
+    end
+    """
+
+    {:ok, ast} = Parser.parse_module(str)
+    result = ErlTranslate.translate(ast, "test", "/tmp/foo")
+
+    forms = [
+      {:attribute, 1, :file, {'/tmp/foo', 1}},
+      {:attribute, 1, :module, :test},
+      {:attribute, 5, :export, [bar: 0]},
+      {:attribute, 1, :export, [foo: 1]},
+      {:function, 5, :bar, 0,
+       [{:clause, 5, [], '', [{:call, 4, {:atom, 4, :foo}, [{:var, 4, :x}]}]}]},
+      {:function, 1, :foo, 1,
+       [
+         {:clause, 1, [{:var, 1, :x}], '',
+          [{:call, 1, {:remote, 1, {:atom, 1, :Test}, {:atom, 1, :foo}}, [{:var, 1, :x}]}]}
+       ]}
+    ]
+
+    assert result == forms
+  end
 end
diff --git a/test/fika/compiler/parser_test.exs b/test/fika/compiler/parser_test.exs
index 5e1ec4f..f31b545 100644
--- a/test/fika/compiler/parser_test.exs
+++ b/test/fika/compiler/parser_test.exs
@@ -2,6 +2,7 @@ defmodule Fika.Compiler.ParserTest do
   use ExUnit.Case, async: true
 
   alias Fika.Compiler.TypeChecker.Types, as: T
+  alias Fika.Compiler.Parser
 
   test "integer" do
     str = """
@@ -356,6 +357,31 @@ defmodule Fika.Compiler.ParserTest do
                   ]}
                ]}} = TestParser.function_def!(code)
     end
+
+    test "external function" do
+      str = """
+      ext foo(x: Int, y: Int) : Int = {"Elixir.Test", "foo", [x, y]}
+      """
+
+      assert {
+               :function,
+               [position: {1, 0, 62}],
+               {
+                 :foo,
+                 [
+                   {{:identifier, {1, 0, 9}, :x}, {:type, {1, 0, 14}, :Int}},
+                   {{:identifier, {1, 0, 17}, :y}, {:type, {1, 0, 22}, :Int}}
+                 ],
+                 {:type, {1, 0, 29}, :Int},
+                 [
+                   {:ext_call, {1, 0, 62},
+                    {Test, :foo, [{:identifier, {1, 0, 57}, :x}, {:identifier, {1, 0, 60}, :y}],
+                     :Int}}
+                 ]
+               }
+             } ==
+               TestParser.function_def!(str)
+    end
   end
 
   describe "if-else expression" do
@@ -1065,6 +1091,59 @@ defmodule Fika.Compiler.ParserTest do
     end
   end
 
+  test "parsing modules" do
+    str = """
+    use foo/bar
+
+    ext ext1 = {"Test", "foo", []}
+    ext ext2(x: Int, y: Int) : Int = {"Elixir.Test", "foo", [x, y]}
+
+    fn foo do
+      bar.baz()
+    end
+
+    fn foo2 do
+      123
+    end
+    """
+
+    assert Parser.parse_module(str) == {
+             :ok,
+             [
+               use_modules: [{"foo/bar", {1, 0, 11}}],
+               function_defs: [
+                 {:function, [position: {3, 13, 43}],
+                  {:ext1, [], {:type, {3, 13, 21}, :Nothing},
+                   [{:ext_call, {3, 13, 43}, {:Test, :foo, [], :Nothing}}]}},
+                 {
+                   :function,
+                   [position: {4, 44, 107}],
+                   {
+                     :ext2,
+                     [
+                       {{:identifier, {4, 44, 54}, :x}, {:type, {4, 44, 59}, :Int}},
+                       {{:identifier, {4, 44, 62}, :y}, {:type, {4, 44, 67}, :Int}}
+                     ],
+                     {:type, {4, 44, 74}, :Int},
+                     [
+                       {:ext_call, {4, 44, 107},
+                        {Test, :foo,
+                         [{:identifier, {4, 44, 102}, :x}, {:identifier, {4, 44, 105}, :y}],
+                         :Int}}
+                     ]
+                   }
+                 },
+                 {:function, [position: {8, 131, 134}],
+                  {:foo, [], {:type, {6, 109, 115}, :Nothing},
+                   [{:call, {:baz, {7, 119, 130}}, [], "foo/bar"}]}},
+                 {:function, [position: {12, 153, 156}],
+                  {:foo2, [], {:type, {10, 136, 143}, :Nothing},
+                   [{:integer, {11, 147, 152}, 123}]}}
+               ]
+             ]
+           }
+  end
+
   describe "identifiers" do
     test "cannot be keywords" do
       keywords = ["fn", "do", "end", "if", "else"]
diff --git a/test/fika/compiler/type_checker_test.exs b/test/fika/compiler/type_checker_test.exs
index f9ef7e9..d50c5dc 100644
--- a/test/fika/compiler/type_checker_test.exs
+++ b/test/fika/compiler/type_checker_test.exs
@@ -74,6 +74,23 @@ defmodule Fika.Compiler.TypeCheckerTest do
     assert {:error, "Unknown variable: foo"} = TypeChecker.infer_exp(%{}, ast)
   end
 
+  test "infer ext function's return type" do
+    str = """
+    ext foo(a: Int) : String = {"Test", "foo", [a]}
+
+    fn bar(x: Int) do
+      foo(x)
+    end
+    """
+
+    {:ok, ast} = Parser.parse_module(str)
+    env = TypeChecker.init_env(ast)
+
+    [_foo, bar] = ast[:function_defs]
+
+    assert {:ok, :String} = TypeChecker.infer(bar, env)
+  end
+
   test "infer function's return type" do
     str = """
     fn foo(a: Int) do
@@ -525,7 +542,7 @@ defmodule Fika.Compiler.TypeCheckerTest do
       assert {:ok, :Bool} = TypeChecker.infer(function, %{})
     end
 
-    test "when function returns is expected to return an union type and has if-else clause" do
+    test "when function returns is expected to return a union type and has if-else clause" do
       str = """
       use test2