Repository: fika-lang/fika
Pull request #42 (2020-10-30 00:09:11)

Feat: add comparison operators for Int
Support limited to integers only, as agreed.


Patch:
diff --git a/example.fi b/example.fi
index 816a250..aa66648 100644
--- a/example.fi
+++ b/example.fi
@@ -74,3 +74,12 @@ end
 fn unary_minus(a: Int) : Int do
   -a
 end
+
+# Comparison operators
+fn max(a: Int, b: Int) : Int do
+  if a > b do
+    a
+  else
+    b
+  end
+end
\ No newline at end of file
diff --git a/lib/fika/env.ex b/lib/fika/env.ex
index 567f75d..bffed92 100644
--- a/lib/fika/env.ex
+++ b/lib/fika/env.ex
@@ -29,7 +29,13 @@ defmodule Fika.Env do
         "kernel.|(Bool,Bool)" => "Bool",
         "kernel.&(Bool,Bool)" => "Bool",
         "kernel.!(Bool)" => "Bool",
-        "kernel.-(Int)" => "Int"
+        "kernel.-(Int)" => "Int",
+        "kernel.<(Int,Int)" => "Bool",
+        "kernel.>(Int,Int)" => "Bool",
+        "kernel.<=(Int,Int)" => "Bool",
+        "kernel.>=(Int,Int)" => "Bool",
+        "kernel.==(Int,Int)" => "Bool",
+        "kernel.!=(Int,Int)" => "Bool"
       }
     }
   end
diff --git a/lib/fika/erl_translate.ex b/lib/fika/erl_translate.ex
index bb41552..4b8bd62 100644
--- a/lib/fika/erl_translate.ex
+++ b/lib/fika/erl_translate.ex
@@ -36,10 +36,18 @@ defmodule Fika.ErlTranslate do
   end
 
   defp translate_exp({:call, {bin_op, {line, _, _}}, [arg1, arg2], _module})
-       when bin_op in [:+, :-, :*, :/] do
+       when bin_op in [:+, :-, :*, :/, :<, :>, :>=, :==] do
     {:op, line, bin_op, translate_exp(arg1), translate_exp(arg2)}
   end
 
+  defp translate_exp({:call, {:<=, {line, _, _}}, [arg1, arg2], _module}) do
+    {:op, line, :"=<", translate_exp(arg1), translate_exp(arg2)}
+  end
+
+  defp translate_exp({:call, {:!=, {line, _, _}}, [arg1, arg2], _module}) do
+    {:op, line, :"/=", translate_exp(arg1), translate_exp(arg2)}
+  end
+
   defp translate_exp({:call, {:!, {line, _, _}}, [arg], _module}) do
     {:op, line, :not, translate_exp(arg)}
   end
diff --git a/lib/fika/parser/expressions.ex b/lib/fika/parser/expressions.ex
index 7a7e176..5e48d05 100644
--- a/lib/fika/parser/expressions.ex
+++ b/lib/fika/parser/expressions.ex
@@ -33,32 +33,76 @@ defmodule Fika.Parser.Expressions do
       factor
     ])
 
-  term =
+  exp_mult_op =
     unary_exp
     |> optional(
       allow_horizontal_space
-      |> choice([string("*"), string("/"), string("&")])
+      |> choice([string("*"), string("/")])
       |> concat(allow_space)
-      |> parsec(:term)
+      |> parsec(:exp_mult_op)
     )
 
-  exp_mult_op = Helper.to_ast(term, :exp_bin_op)
+  exp_mult = Helper.to_ast(exp_mult_op, :exp_bin_op)
 
-  exp_bin_op =
-    exp_mult_op
+  exp_add_op =
+    exp_mult
     |> optional(
       allow_horizontal_space
-      |> choice([string("+"), string("-"), string("|")])
+      |> choice([string("+"), string("-")])
       |> concat(allow_space)
-      |> parsec(:exp_bin_op)
+      |> parsec(:exp_add_op)
     )
 
-  exp_add_op = Helper.to_ast(exp_bin_op, :exp_bin_op)
+  exp_add = Helper.to_ast(exp_add_op, :exp_bin_op)
+
+  exp_rel_op =
+    exp_add
+    |> optional(
+      allow_horizontal_space
+      |> choice([string("<="), string(">="), string("<"), string(">")])
+      |> concat(allow_space)
+      |> parsec(:exp_rel_op)
+    )
+
+  exp_rel = Helper.to_ast(exp_rel_op, :exp_bin_op)
+
+  exp_comp_op =
+    exp_rel
+    |> optional(
+      allow_horizontal_space
+      |> choice([string("=="), string("!=")])
+      |> concat(allow_space)
+      |> parsec(:exp_comp_op)
+    )
+
+  exp_comp = Helper.to_ast(exp_comp_op, :exp_bin_op)
+
+  exp_and_op =
+    exp_comp
+    |> optional(
+      allow_horizontal_space
+      |> concat(string("&"))
+      |> concat(allow_space)
+      |> parsec(:exp_and_op)
+    )
+
+  exp_and = Helper.to_ast(exp_and_op, :exp_bin_op)
+
+  exp_or_op =
+    exp_and
+    |> optional(
+      allow_horizontal_space
+      |> concat(string("|"))
+      |> concat(allow_space)
+      |> parsec(:exp_or_op)
+    )
+
+  exp_or = Helper.to_ast(exp_or_op, :exp_bin_op)
 
   exp =
     choice([
       exp_match,
-      exp_add_op
+      exp_or
     ])
     |> label("expression")
 
@@ -74,8 +118,12 @@ defmodule Fika.Parser.Expressions do
       |> parsec(:exps)
     )
 
-  defcombinatorp :exp_bin_op, exp_bin_op
-  defcombinatorp :term, term
+  defcombinatorp :exp_mult_op, exp_mult_op
+  defcombinatorp :exp_add_op, exp_add_op
+  defcombinatorp :exp_rel_op, exp_rel_op
+  defcombinatorp :exp_comp_op, exp_comp_op
+  defcombinatorp :exp_and_op, exp_and_op
+  defcombinatorp :exp_or_op, exp_or_op
   defcombinator :factor, factor
   defcombinator :exp, exp
   defcombinator :exps, exps
diff --git a/lib/fika/parser/helper.ex b/lib/fika/parser/helper.ex
index e4db4ef..6fa8677 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
@@ -36,7 +36,7 @@ defmodule Fika.Parser.Helper do
   end
 
   def do_to_ast({[unary_op, exp], line}, :unary_op)
-      when unary_op in ["!", "+", "-"] do
+      when unary_op in ["!", "-"] do
     {:call, {String.to_atom(unary_op), line}, [exp], :kernel}
   end
 


Test patch:
diff --git a/test/fika/parser_test.exs b/test/fika/parser_test.exs
index 09ff772..cba3158 100644
--- a/test/fika/parser_test.exs
+++ b/test/fika/parser_test.exs
@@ -44,13 +44,36 @@ defmodule Fika.ParserTest do
     # ! -         Unary           Boolean not and arithmetic unary minus
     # * /         Left            Arithmetic binary mult and div
     # + -         Left            Arithmetic binary plus and minus
-    # < > <= >=   Left            Comparison                                Implement
-    # == !=       Left            Comparison                                Implement
+    # < > <= >=   Left            Comparison
+    # == !=       Left            Comparison
     # &           Left            Boolean AND
     # |           Left            Boolean OR
     # =           Right           Match operator                            Parser refactoring
     # &           Unary           Capture operator                          Parser refactoring
 
+    # These operators cannot be combined to form a valid expression but we test the parser anyway
+    test "arithmetic binary * and / have less precedence than unary !" do
+      assert {
+               :call,
+               {:*, {1, 0, 14}},
+               [
+                 {:integer, {1, 0, 1}, 1},
+                 {:call, {:!, {1, 0, 14}}, [{:identifier, {1, 0, 14}, :something}], :kernel}
+               ],
+               :kernel
+             } == TestParser.expression!("1 * !something")
+
+      assert {
+               :call,
+               {:/, {1, 0, 14}},
+               [
+                 {:call, {:!, {1, 0, 10}}, [{:identifier, {1, 0, 10}, :something}], :kernel},
+                 {:integer, {1, 0, 14}, 2}
+               ],
+               :kernel
+             } == TestParser.expression!("!something / 2")
+    end
+
     test "arithmetic binary * and / have less precedence than unary -" do
       assert {:ok, {-2, "Int", _}} = TE.eval("1 * -2")
       assert {:ok, {2.0, "Float", _}} = TE.eval("-10 / -5")
@@ -61,8 +84,51 @@ defmodule Fika.ParserTest do
       assert {:ok, {3.0, "Float", _}} = TE.eval("1 * 5 - 4 / 2")
     end
 
-    test "boolean & has less precedence than !" do
-      assert {:ok, {false, "Bool", _}} = TE.eval("!true & :false")
+    test "comparison operators <, >, <= and >= have less precedence than arithmetic + and -" do
+      assert {:ok, {false, "Bool", _}} = TE.eval("1 + 2 < 4 - 1")
+      assert {:ok, {true, "Bool", _}} = TE.eval("1 + 2 <= 4 - 1")
+      assert {:ok, {false, "Bool", _}} = TE.eval("5 - 2 > 2 + 1")
+      assert {:ok, {true, "Bool", _}} = TE.eval("5 - 2 >= 2 + 1")
+    end
+
+    # TODO: Test using evaluation instead of AST as soon as == and != support booleans.
+    #   Replace the content of this test with something like:
+    #     assert {:ok, {true, "Bool", _}} = TE.eval("1 < 1 == 2 > 2")
+    #     assert {:ok, {true, "Bool", _}} = TE.eval("1 <= 1 != 2 >= 3")
+    test "comparison operators == and != have less precedence than <, >, <=, >=" do
+      # Just for now, we test the parser for precedence
+      assert {
+               :call,
+               {:==, {1, 0, 14}},
+               [
+                 {:call, {:<, {1, 0, 5}}, [{:integer, {1, 0, 1}, 1}, {:integer, {1, 0, 5}, 1}],
+                  :kernel},
+                 {:call, {:>, {1, 0, 14}}, [{:integer, {1, 0, 10}, 2}, {:integer, {1, 0, 14}, 2}],
+                  :kernel}
+               ],
+               :kernel
+             } == TestParser.expression!("1 < 1 == 2 > 2")
+
+      assert {
+               :call,
+               {:!=, {1, 0, 16}},
+               [
+                 {:call, {:<=, {1, 0, 6}}, [{:integer, {1, 0, 1}, 1}, {:integer, {1, 0, 6}, 1}],
+                  :kernel},
+                 {:call, {:>=, {1, 0, 16}},
+                  [{:integer, {1, 0, 11}, 2}, {:integer, {1, 0, 16}, 3}], :kernel}
+               ],
+               :kernel
+             } == TestParser.expression!("1 <= 1 != 2 >= 3")
+
+      # Then we test == and != actually work
+      assert {:ok, {true, "Bool", _}} = TE.eval("1 == 1")
+      assert {:ok, {false, "Bool", _}} = TE.eval("1 != 1")
+    end
+
+    test "boolean & has less precedence than == and !=" do
+      assert {:ok, {true, "Bool", _}} = TE.eval("1 == 1 & 1 != 2")
+      assert {:ok, {false, "Bool", _}} = TE.eval("2 == 1 & 1 != 2")
     end
 
     test "boolean | has less precedence than &" do
@@ -73,6 +139,7 @@ defmodule Fika.ParserTest do
     test "expressions in parenthesis have higher precedence than anything else" do
       assert {:ok, {1, "Int", _}} = TE.eval("-(-1)")
       assert {:ok, {6, "Int", _}} = TE.eval("(3 - 1) * (1 + 2)")
+      assert {:ok, {true, "Bool", _}} = TE.eval("2 >= (1 + 1)")
       assert {:ok, {true, "Bool", _}} = TE.eval("true & (false | true)")
     end
   end
@@ -91,9 +158,13 @@ defmodule Fika.ParserTest do
       assert {:ok, {2.0, "Float", _}} = TE.eval("4#{TP.h_space()}/#{TP.space()}2")
       assert {:ok, {7, "Int", _}} = TE.eval("3#{TP.h_space()}+#{TP.space()}4")
       assert {:ok, {1, "Int", _}} = TE.eval("3#{TP.h_space()}-#{TP.space()}2")
-
+      assert {:ok, {true, "Bool", _}} = TE.eval("2#{TP.h_space()}<#{TP.space()}3")
+      assert {:ok, {true, "Bool", _}} = TE.eval("2#{TP.h_space()}<=#{TP.space()}3")
+      assert {:ok, {true, "Bool", _}} = TE.eval("2#{TP.h_space()}>#{TP.space()}1")
+      assert {:ok, {true, "Bool", _}} = TE.eval("2#{TP.h_space()}>=#{TP.space()}1")
+      assert {:ok, {true, "Bool", _}} = TE.eval("2#{TP.h_space()}==#{TP.space()}2")
+      assert {:ok, {true, "Bool", _}} = TE.eval("2#{TP.h_space()}!=#{TP.space()}1")
       assert {:ok, {true, "Bool", _}} = TE.eval("true#{TP.h_space()}&#{TP.space()}true")
-
       assert {:ok, {true, "Bool", _}} = TE.eval("false#{TP.h_space()}|#{TP.space()}true")
     end
 
@@ -105,9 +176,13 @@ defmodule Fika.ParserTest do
       assert {:error, ^msg, _, _, _, _} = TE.eval("3#{TP.space()}/#{TP.space()}2")
       assert {:error, ^msg, _, _, _, _} = TE.eval("3#{TP.space()}+#{TP.space()}4")
       assert {:error, ^msg, _, _, _, _} = TE.eval("3#{TP.space()}-#{TP.space()}2")
-
+      assert {:error, ^msg, _, _, _, _} = TE.eval("2#{TP.space()}<#{TP.space()}3")
+      assert {:error, ^msg, _, _, _, _} = TE.eval("2#{TP.space()}<=#{TP.space()}3")
+      assert {:error, ^msg, _, _, _, _} = TE.eval("2#{TP.space()}>#{TP.space()}1")
+      assert {:error, ^msg, _, _, _, _} = TE.eval("2#{TP.space()}>=#{TP.space()}1")
+      assert {:error, ^msg, _, _, _, _} = TE.eval("2#{TP.space()}==#{TP.space()}2")
+      assert {:error, ^msg, _, _, _, _} = TE.eval("2#{TP.space()}!=#{TP.space()}1")
       assert {:error, ^msg, _, _, _, _} = TE.eval("true#{TP.space()}&#{TP.space()}true")
-
       assert {:error, ^msg, _, _, _, _} = TE.eval("false#{TP.space()}|#{TP.space()}true")
     end