Repository: fika-lang/fika
Pull request #28 (2020-10-04 21:28:22)

Add logical operators
Adds `&&`, `||` and `!` logical operators.

I think there are some improvements that can be done, but I'm opening the PR so we have a better starting point for discussion.

emilsoman: @v0idpwn as per the latest discussion in discord, we have agreed that for now, we'll keep :true and :false as atoms in our AST. Going forward, we may decide based on feedback from actual usage, to remove boolean keywords and use :true and :false as bools.

Documenting @polvalente 's concern:
> For anyone either coming from the BEAM or going from Fika to the lower levels, it would be really hard to differentiate both (as it is being for me).
...
what would lead someone to use :true and :false instead of the respective booleans?



Patch:
diff --git a/example.fi b/example.fi
index eea252a..1663bc7 100644
--- a/example.fi
+++ b/example.fi
@@ -37,3 +37,15 @@ end
 fn tuple : {Int,Bool} do
   {1, true}
 end
+
+fn or(a: Bool, b: Bool) : Bool do
+  a | b
+end
+
+fn and(a: Bool, b: Bool) : Bool do
+  a & b
+end
+
+fn not(a: Bool) : Bool do
+  !a
+end
diff --git a/lib/fika/env.ex b/lib/fika/env.ex
index 00a130e..24dece6 100644
--- a/lib/fika/env.ex
+++ b/lib/fika/env.ex
@@ -25,7 +25,10 @@ defmodule Fika.Env do
         "kernel./(Int,Int)" => "Float",
         "kernel./(Int,Float)" => "Float",
         "kernel./(Float,Int)" => "Float",
-        "kernel./(Float,Float)" => "Float"
+        "kernel./(Float,Float)" => "Float",
+        "kernel.|(Bool,Bool)" => "Bool",
+        "kernel.&(Bool,Bool)" => "Bool",
+        "kernel.!(Bool)" => "Bool"
       }
     }
   end
diff --git a/lib/fika/erl_translate.ex b/lib/fika/erl_translate.ex
index d72d1f2..c316a56 100644
--- a/lib/fika/erl_translate.ex
+++ b/lib/fika/erl_translate.ex
@@ -40,6 +40,18 @@ defmodule Fika.ErlTranslate do
     {:op, line, bin_op, translate_exp(arg1), translate_exp(arg2)}
   end
 
+  defp translate_exp({:call, {:|, {line, _, _}}, [arg1, arg2], _module}) do
+    {:op, line, :or, translate_exp(arg1), translate_exp(arg2)}
+  end
+
+  defp translate_exp({:call, {:&, {line, _, _}}, [arg1, arg2], _module}) do
+    {:op, line, :and, translate_exp(arg1), translate_exp(arg2)}
+  end
+
+  defp translate_exp({:call, {:!, {line, _, _}}, [arg1], _module}) do
+    {:op, line, :not, translate_exp(arg1)}
+  end
+
   defp translate_exp({:call, {name, {line, _, _}}, args, nil}) do
     {:call, line, {:atom, line, name}, translate_exps(args)}
   end
diff --git a/lib/fika/expressions.ex b/lib/fika/expressions.ex
index 2d86e52..fb84b93 100644
--- a/lib/fika/expressions.ex
+++ b/lib/fika/expressions.ex
@@ -4,14 +4,14 @@ defmodule Fika.Parser.Expressions do
   alias Fika.Parser.{Common, Helper, LiteralExps, NonLiteralExps}
 
   allow_space = parsec({Common, :allow_space})
-  horizontal_space = parsec({Common, :horizontal_space})
+  allow_horizontal_space = parsec({Common, :allow_horizontal_space})
   identifier = parsec({Common, :identifier})
   literal_exps = parsec({LiteralExps, :literal_exps})
   non_literal_exps = parsec({NonLiteralExps, :non_literal_exps})
 
   exp_match =
     identifier
-    |> concat(allow_space)
+    |> concat(allow_horizontal_space)
     |> ignore(string("="))
     |> concat(allow_space)
     |> parsec(:exp)
@@ -27,8 +27,8 @@ defmodule Fika.Parser.Expressions do
   term =
     factor
     |> optional(
-      allow_space
-      |> choice([string("*"), string("/")])
+      allow_horizontal_space
+      |> choice([string("*"), string("/"), string("&")])
       |> concat(allow_space)
       |> parsec(:term)
     )
@@ -38,8 +38,8 @@ defmodule Fika.Parser.Expressions do
   exp_bin_op =
     exp_mult_op
     |> optional(
-      allow_space
-      |> choice([string("+"), string("-")])
+      allow_horizontal_space
+      |> choice([string("+"), string("-"), string("|")])
       |> concat(allow_space)
       |> parsec(:exp_bin_op)
     )
@@ -54,9 +54,9 @@ defmodule Fika.Parser.Expressions do
     |> label("expression")
 
   exp_delimiter =
-    ignore(repeat(horizontal_space))
+    allow_horizontal_space
     |> ignore(times(choice([string("\n"), string(";")]), min: 1))
-    |> ignore(repeat(horizontal_space))
+    |> concat(allow_horizontal_space)
 
   exps =
     parsec(:exp)
diff --git a/lib/fika/parser/common.ex b/lib/fika/parser/common.ex
index d513399..2f5e54b 100644
--- a/lib/fika/parser/common.ex
+++ b/lib/fika/parser/common.ex
@@ -38,6 +38,11 @@ defmodule Fika.Parser.Common do
     |> times(min: 1)
     |> ignore()
 
+  allow_horizontal_space =
+    horizontal_space
+    |> repeat()
+    |> ignore()
+
   allow_space =
     space
     |> repeat()
@@ -65,6 +70,6 @@ defmodule Fika.Parser.Common do
   defcombinator :require_space, require_space
   defcombinator :identifier_str, identifier_str
   defcombinator :identifier, identifier
-  defcombinator :horizontal_space, horizontal_space
+  defcombinator :allow_horizontal_space, allow_horizontal_space
   defcombinator :atom, atom
 end
diff --git a/lib/fika/parser/helper.ex b/lib/fika/parser/helper.ex
index 34cc38a..8222b5e 100644
--- a/lib/fika/parser/helper.ex
+++ b/lib/fika/parser/helper.ex
@@ -26,7 +26,7 @@ defmodule Fika.Parser.Helper do
   end
 
   def do_to_ast({[left, bin_op, right | rest], line}, :exp_bin_op)
-      when bin_op in ["+", "-", "*", "/"] do
+      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)
   end
@@ -35,6 +35,10 @@ defmodule Fika.Parser.Helper do
     result
   end
 
+  def do_to_ast({["!", exp], line}, :not) do
+    {:call, {:!, line}, [exp], :kernel}
+  end
+
   def do_to_ast({[name], line}, :identifier) do
     {:identifier, line, String.to_atom(name)}
   end
@@ -135,6 +139,10 @@ defmodule Fika.Parser.Helper do
     end
   end
 
+  def do_to_ast({[{:identifier, line, value}], line}, :atom) when value in [true, false] do
+    {:boolean, line, value}
+  end
+
   def do_to_ast({[{:identifier, line, value}], line}, :atom) do
     {:atom, line, value}
   end
diff --git a/lib/fika/parser/non_literal_exps.ex b/lib/fika/parser/non_literal_exps.ex
index 4fffe97..4ee226a 100644
--- a/lib/fika/parser/non_literal_exps.ex
+++ b/lib/fika/parser/non_literal_exps.ex
@@ -72,8 +72,14 @@ defmodule Fika.Parser.NonLiteralExps do
     |> wrap(call_args)
     |> ignore(string(")"))
 
+  not_op =
+    string("!")
+    |> concat(exp)
+    |> Helper.to_ast(:not)
+
   non_literal_exps =
     choice([
+      not_op,
       exp_paren,
       function_call,
       identifier,


Test patch:
diff --git a/test/fika/erl_translate_test.exs b/test/fika/erl_translate_test.exs
index a6129dd..77d82fa 100644
--- a/test/fika/erl_translate_test.exs
+++ b/test/fika/erl_translate_test.exs
@@ -67,6 +67,24 @@ defmodule Fika.ErlTranslateTest do
     assert result == forms
   end
 
+  test "logical operators" do
+    str = "(true | !false) & true"
+
+    ast = TestParser.expression!(str)
+
+    assert {:op, 1, :and, {:op, 1, :or, {:atom, 1, true}, {:op, 1, :not, {:atom, 1, false}}},
+            {:atom, 1, true}} = ErlTranslate.translate_expression(ast)
+  end
+
+  test "logical operators precedence" do
+    str = "false | true & !true"
+    ast = TestParser.expression!(str)
+
+    assert {:op, _, :or, {:atom, _, false},
+            {:op, _, :and, {:atom, _, true}, {:op, _, :not, {:atom, _, true}}}} =
+             ErlTranslate.translate_expression(ast)
+  end
+
   test "record" do
     str = "{foo: 1}"
     ast = TestParser.expression!(str)
diff --git a/test/fika/parser_test.exs b/test/fika/parser_test.exs
index ff6f3ec..5110577 100644
--- a/test/fika/parser_test.exs
+++ b/test/fika/parser_test.exs
@@ -682,6 +682,49 @@ defmodule Fika.ParserTest do
     end
   end
 
+  describe "logical operators" do
+    test "supports negation" do
+      assert {:call, {:!, _}, [{:boolean, _, true}], kernel} = TestParser.expression!("!true")
+    end
+
+    test "supports negation with more complex expressions" do
+      {
+        :call,
+        {:!, _},
+        [
+          {:call, {:|, _}, [{:boolean, _, true}, {:boolean, _, false}], :kernel}
+        ],
+        :kernel
+      } = TestParser.expression!("!(true | false)")
+    end
+
+    test "simple usage" do
+      str = "false | true"
+      result = TestParser.expression!(str)
+
+      assert {:call, {:|, _},
+              [
+                {:boolean, _, false},
+                {:boolean, _, true}
+              ], :kernel} = result
+    end
+
+    test "more complex expressions" do
+      str = "true & (false | :true)"
+      result = TestParser.expression!(str)
+
+      assert {
+               :call,
+               {:&, _},
+               [
+                 {:boolean, _, true},
+                 {:call, {:|, _}, [{:boolean, _, false}, {:boolean, _, true}], :kernel}
+               ],
+               :kernel
+             } = result
+    end
+  end
+
   describe "expression delimiter" do
     test "cannot have two assignments in the same line" do
       str = "x = 1 y = 2"
@@ -718,6 +761,53 @@ defmodule Fika.ParserTest do
       """
 
       assert {:ok, [_], _, _, _, _} = TestParser.exps(str)
+
+      str = """
+      x = foo &
+        bar
+      """
+
+      assert {:ok, [_], _, _, _, _} = TestParser.exps(str)
+    end
+
+    test "function ref on new line" do
+      str = """
+      x = foo
+        &bar
+      """
+
+      assert {:ok, [_, {:function_ref, _, _}], _, _, _, _} = TestParser.exps(str)
+    end
+
+    # TODO: This actually looks cleaner, so we may eventually allow this
+    # by adding more fine grained rules to our parser.
+    test "| on newline is an error" do
+      str = """
+      x = foo
+        | bar
+        | baz
+      """
+
+      assert {:error, _, _, _, _, _} = TestParser.exps(str)
+    end
+
+    test "| on same line is ok" do
+      str = """
+      x = foo |
+        bar |
+        baz
+      """
+
+      assert {:ok, [_], _, _, _, _} = TestParser.exps(str)
+    end
+
+    test "= on newline is an error" do
+      str = """
+      x
+      = 123
+      """
+
+      assert {:error, _, _, _, _, _} = TestParser.exps(str)
     end
   end
 end
diff --git a/test/fika/type_checker_test.exs b/test/fika/type_checker_test.exs
index fb20b14..4d38859 100644
--- a/test/fika/type_checker_test.exs
+++ b/test/fika/type_checker_test.exs
@@ -40,6 +40,31 @@ defmodule Fika.TypeCheckerTest do
     assert {:ok, "Int", _} = TypeChecker.infer_exp(Env.init(), ast)
   end
 
+  describe "logical operators" do
+    test "infer type of logical expressions" do
+      # and
+      str = "true & false"
+      ast = TestParser.expression!(str)
+      assert {:ok, "Bool", _} = TypeChecker.infer_exp(Env.init(), ast)
+
+      # or
+      str = "true | false"
+      ast = TestParser.expression!(str)
+      assert {:ok, "Bool", _} = TypeChecker.infer_exp(Env.init(), ast)
+
+      # negation
+      str = "!true"
+      ast = TestParser.expression!(str)
+      assert {:ok, "Bool", _} = TypeChecker.infer_exp(Env.init(), ast)
+    end
+
+    test "infer type of logical expressions when using atoms" do
+      str = "true & :false"
+      ast = TestParser.expression!(str)
+      assert {:ok, "Bool", _} = TypeChecker.infer_exp(Env.init(), ast)
+    end
+  end
+
   test "infer undefined variable" do
     str = "foo"