Repository: fika-lang/fika (elixir)

Issue:
Add if-else expression
#### Syntax:
```
if <exp> do
  exps
else
  exps
end
```
if is always followed by an else. This can be parsed into the AST node `{{:if, line}, condition_exp, true_exps, false_exps}`

#### Type checking:
The true_exps and false_exps can return two different things which make the return type a union type (example: `{:ok, Int} | {:error, String}`), but we don't have union types now, so for now we'll just make sure they both return the same type. In short, the type checks needed for now:

1. The condition_exp should be a boolean.
2. The return types of true_exps and false_exps should be the same.

Once these checks pass, the type of the if-else expression is inferred as the type of the true_exps or false_exps blocks.

#### Erl translate
In Erl Abs form, this can be represented as a `case` node.

Pull request description:
Add if-else expression
Closes #8 

Patch:
diff --git a/README.md b/README.md
index 1b2d82a..c8bed4f 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,17 @@ sum(40, 2)
 example.sum(40, 2)
 ```
 
+#### If-Else expressions
+An if-else expression in Fika looks like this:
+```
+if true do
+  a = "we have if-else now!"
+  200
+else
+  404
+end
+```
+
 #### Basic types and operators
 
 Fika is currently a proof of concept so its data types and operators are quite
diff --git a/example.fi b/example.fi
index 0d87fc4..f41b84e 100644
--- a/example.fi
+++ b/example.fi
@@ -25,3 +25,11 @@ end
 fn bool : Bool do
   true
 end
+
+fn simple_if : String do
+  if true do
+    "this is a passing if!"
+  else
+    "you can't see me \m/"
+  end
+end
diff --git a/lib/fika/erl_translate.ex b/lib/fika/erl_translate.ex
index 7154590..eaf7e2e 100644
--- a/lib/fika/erl_translate.ex
+++ b/lib/fika/erl_translate.ex
@@ -99,6 +99,18 @@ defmodule Fika.ErlTranslate do
     {:fun, line, f}
   end
 
+  defp translate_exp({{:if, {line, _, _}}, condition, if_block, else_block}) do
+    {
+      :case,
+      line,
+      translate_exp(condition),
+      [
+        {:clause, line, [{:atom, line, true}], [], translate_exps(if_block)},
+        {:clause, line, [{:atom, line, false}], [], translate_exps(else_block)}
+      ]
+    }
+  end
+
   defp add_record_meta(k_vs, name, line) do
     name =
       if name do
diff --git a/lib/fika/parser.ex b/lib/fika/parser.ex
index e6c4c98..9dc8e39 100644
--- a/lib/fika/parser.ex
+++ b/lib/fika/parser.ex
@@ -38,7 +38,9 @@ defmodule Fika.Parser do
     choice([
       string("fn"),
       string("do"),
-      string("end")
+      string("end"),
+      string("if"),
+      string("else")
     ])
 
   identifier_str =
@@ -199,6 +201,23 @@ defmodule Fika.Parser do
     |> wrap(call_args)
     |> ignore(string(")"))
 
+  exp_if_else =
+    ignore(string("if"))
+    |> concat(require_space)
+    |> parsec(:exp)
+    |> concat(require_space)
+    |> ignore(string("do"))
+    |> concat(require_space)
+    |> wrap(parsec(:exps))
+    |> concat(require_space)
+    |> ignore(string("else"))
+    |> concat(require_space)
+    |> wrap(parsec(:exps))
+    |> concat(require_space)
+    |> ignore(string("end"))
+    |> label("if-else expression")
+    |> Helper.to_ast(:exp_if_else)
+
   literal_exps =
     choice([
       integer,
@@ -206,7 +225,8 @@ defmodule Fika.Parser do
       string_exp,
       exp_list,
       record,
-      function_ref
+      function_ref,
+      exp_if_else
     ])
 
   non_literal_exps =
diff --git a/lib/fika/parser_helper.ex b/lib/fika/parser_helper.ex
index 431451a..5144d40 100644
--- a/lib/fika/parser_helper.ex
+++ b/lib/fika/parser_helper.ex
@@ -22,6 +22,10 @@ defmodule Fika.ParserHelper do
     {:boolean, line, value == "true"}
   end
 
+  def do_to_ast({[condition, true_block, false_block], line}, :exp_if_else) do
+    {{:if, line}, condition, true_block, false_block}
+  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], :kernel}
     do_to_ast({[new_left | rest], line}, :exp_bin_op)
diff --git a/lib/fika/type_checker.ex b/lib/fika/type_checker.ex
index 9af9c94..4518385 100644
--- a/lib/fika/type_checker.ex
+++ b/lib/fika/type_checker.ex
@@ -197,6 +197,44 @@ defmodule Fika.TypeChecker do
     end
   end
 
+  # if-else expression
+  def infer_exp(env, {{:if, _line}, condition, if_block, else_block}) do
+    Logger.debug "Inferring an if-else expression"
+
+    case infer_if_else_condition(env, condition) do
+      {:ok, "Bool", env} -> infer_if_else_blocks(env, if_block, else_block)
+      error -> error
+    end
+  end
+
+  defp infer_if_else_condition(env, condition) do
+    case infer_exp(env, condition) do
+      {:ok, "Bool", env} ->
+        Logger.debug("if-else condition has return type: Bool")
+        {:ok, "Bool", env}
+
+      {_, inferred_type, _env} ->
+        Logger.debug("if-else condition has wrong return type: #{inferred_type}")
+        {:error, "Wrong type for if condition. Expected: Bool, Got: #{inferred_type}"}
+    end
+  end
+
+  defp infer_if_else_blocks(env, if_block, else_block) do
+    {_, if_type_val, env} = infer_block(env, if_block)
+    {_, else_type_val, _} = type = infer_block(env, else_block)
+
+    unless if_type_val == else_type_val do
+      Logger.debug("if and else block have different types")
+
+      error = "Expected if and else blocks to have same return type. " <>
+        "Got #{if_type_val} and #{else_type_val}"
+
+      {:error, error}
+    else
+      type
+    end
+  end
+
   defp do_infer_key_values(key_values, env) do
     Enum.reduce_while(key_values, {:ok, [], env}, fn {k, v}, {:ok, acc, env} ->
       case infer_exp(env, v) do


Test patch:
diff --git a/test/fika/erl_translate_test.exs b/test/fika/erl_translate_test.exs
index b59d06d..b89063e 100644
--- a/test/fika/erl_translate_test.exs
+++ b/test/fika/erl_translate_test.exs
@@ -99,7 +99,26 @@ defmodule Fika.ErlTranslateTest do
     str = "true"
     ast = Parser.expression!(str)
     result = ErlTranslate.translate_expression(ast)
-  
+
     assert result == {:atom, 1, true}
   end
+
+  test "if-else expression" do
+    str =  """
+    if true do
+      "foo"
+    else
+      "bar"
+    end
+    """
+
+    ast = Fika.Parser.expression!(str)
+    result = ErlTranslate.translate_expression(ast)
+    assert {:case, 5, {:atom, 1, true},
+      [
+        {:clause, 5, [{:atom, 5, true}], [], [{:string, 2, 'foo'}]},
+        {:clause, 5, [{:atom, 5, false}], [], [{:string, 4, 'bar'}]}
+      ]
+    } = result
+  end
 end
diff --git a/test/fika/parser_test.exs b/test/fika/parser_test.exs
index 3e61722..78ac303 100644
--- a/test/fika/parser_test.exs
+++ b/test/fika/parser_test.exs
@@ -26,7 +26,6 @@ defmodule Fika.ParserTest do
       assert result ==  {:boolean, {1, 0, 5}, false}
     end
   end
-
   describe "arithmetic" do
     test "arithmetic with add and mult" do
       str = """
@@ -219,6 +218,34 @@ defmodule Fika.ParserTest do
     end
   end
 
+  describe "if-else expression" do
+    test "simple if-else" do
+      str = """
+      if true do
+        a = 1
+        b = 2
+      else
+        c = 3
+      end
+      """
+
+      assert {
+        {:if, {6, 40, 43}}, parsed_condition, parsed_if_block, parsed_else_block,
+      } = Parser.expression!(str)
+
+      assert {:boolean, {1, 0, 7}, true} = parsed_condition
+
+      assert [
+        {{:=, {2, 11, 18}}, {:identifier, {2, 11, 14}, :a}, {:integer, {2, 11, 18}, 1}},
+        {{:=, {3, 19, 26}}, {:identifier, {3, 19, 22}, :b}, {:integer, {3, 19, 26}, 2}}
+      ] = parsed_if_block
+
+      assert [
+        {{:=, {5, 32, 39}}, {:identifier, {5, 32, 35}, :c}, {:integer, {5, 32, 39}, 3}}
+      ] = parsed_else_block
+    end
+  end
+
   describe "match expression" do
     test "simple match" do
       str = """
diff --git a/test/fika/type_checker_test.exs b/test/fika/type_checker_test.exs
index 75d8caa..f47356d 100644
--- a/test/fika/type_checker_test.exs
+++ b/test/fika/type_checker_test.exs
@@ -229,6 +229,75 @@ defmodule Fika.TypeCheckerTest do
     end
   end
 
+  describe "if-else expression" do
+    test "error when condition expression has non-boolean type" do
+      str =  """
+      if "true" do
+        "foo"
+      else
+        "bar"
+      end
+      """
+
+      ast = Fika.Parser.expression!(str)
+      env = Env.init_module_env(Env.init(), "test", ast)
+
+      assert {
+        :error,
+        "Wrong type for if condition. Expected: Bool, Got: String"
+      } = TypeChecker.infer_exp(env, ast)
+    end
+
+    test "completes when if and else blocks have same return type" do
+      str =  """
+      if true do
+        "done"
+      else
+        "500"
+      end
+      """
+
+      ast = Fika.Parser.expression!(str)
+      env = Env.init_module_env(Env.init(), "test", ast)
+
+      assert {:ok, "String", _env} = TypeChecker.infer_exp(env, ast)
+    end
+
+    test "error when if and else blocks have different return types" do
+      str =  """
+      if false do
+        "done"
+      else
+        500
+      end
+      """
+
+      ast = Fika.Parser.expression!(str)
+      env = Env.init_module_env(Env.init(), "test", ast)
+
+      assert {
+        :error,
+        "Expected if and else blocks to have same return type. Got String and Int"
+      } = TypeChecker.infer_exp(env, ast)
+    end
+
+    test "with multiple expressions in blocks" do
+      str =  """
+      if true do
+        a = "done"
+        a
+      else
+        "500"
+      end
+      """
+
+      ast = Fika.Parser.expression!(str)
+      env = Env.init_module_env(Env.init(), "test", ast)
+
+      assert {:ok, "String", _env} = TypeChecker.infer_exp(env, ast)
+    end
+  end
+
   describe "function calls using reference" do
     test "valid reference" do
       str = """