diff --git a/lib/elixir/src/elixir_erl.erl b/lib/elixir/src/elixir_erl.erl index 41a519bf0a4..2a6e04c7213 100644 --- a/lib/elixir/src/elixir_erl.erl +++ b/lib/elixir/src/elixir_erl.erl @@ -74,6 +74,14 @@ elixir_to_erl(Tree, Ann) when is_atom(Tree) -> {atom, Ann, Tree}; elixir_to_erl(Tree, Ann) when is_integer(Tree) -> {integer, Ann, Tree}; +elixir_to_erl(Tree, Ann) when is_float(Tree), Tree == 0.0 -> + % 0.0 needs to be rewritten as the AST for +0.0 in matches + Op = + case <> of + <<1:1,_:63>> -> '-'; + _ -> '+' + end, + {op, Ann, Op, {float, Ann, 0.0}}; elixir_to_erl(Tree, Ann) when is_float(Tree) -> {float, Ann, Tree}; elixir_to_erl(Tree, Ann) when is_binary(Tree) -> diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index 62b1f7c5583..c72d2124362 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -456,6 +456,10 @@ expand(Pid, S, E) when is_pid(Pid) -> {Pid, E} end; +expand(Zero, S, #{context := match} = E) when is_float(Zero), Zero == 0.0 -> + elixir_errors:file_warn([], E, ?MODULE, invalid_match_on_zero_float), + {Zero, S, E}; + expand(Other, S, E) when is_number(Other); is_atom(Other); is_binary(Other) -> {Other, S, E}; @@ -1163,6 +1167,8 @@ guard_context(_) -> "guards". format_error({remote_nullary_no_parens, Expr}) -> String = 'Elixir.String':replace_suffix('Elixir.Macro':to_string(Expr), <<"()">>, <<>>), io_lib:format("parentheses are required for function calls with no arguments, got: ~ts", [String]); +format_error(invalid_match_on_zero_float) -> + "pattern matching on 0.0 is equivalent to matching only on +0.0 from Erlang/OTP 27+. Instead you must match on +0.0 or -0.0"; format_error({useless_literal, Term}) -> io_lib:format("code block contains unused literal ~ts " "(remove the literal or assign it to _ to avoid warnings)", diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index f7042df3a78..8e16612f2df 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -468,6 +468,16 @@ defmodule Kernel.ExpansionTest do end end + describe "floats" do + test "cannot be 0.0 inside match" do + assert capture_io(:stderr, fn -> expand(quote(do: 0.0 = 0.0)) end) =~ + "pattern matching on 0.0 is equivalent to matching only on +0.0 from Erlang/OTP 27+" + + assert {:=, [], [+0.0, +0.0]} = expand(quote(do: +0.0 = 0.0)) + assert {:=, [], [-0.0, +0.0]} = expand(quote(do: -0.0 = 0.0)) + end + end + describe "tuples" do test "expanded as arguments" do assert expand(quote(do: {after_expansion = 1, a})) == quote(do: {after_expansion = 1, a()}) @@ -715,8 +725,11 @@ defmodule Kernel.ExpansionTest do expand(quote(do: [1] ++ 2 ++ [3] = [1, 2, 3])) end) - assert {:=, _, [-1, {{:., _, [:erlang, :-]}, _, [1]}]} = expand(quote(do: -1 = -1)) - assert {:=, _, [1, {{:., _, [:erlang, :+]}, _, [1]}]} = expand(quote(do: +1 = +1)) + assert {:=, _, [-1, {{:., _, [:erlang, :-]}, _, [1]}]} = + expand(quote(do: -1 = -1)) + + assert {:=, _, [1, {{:., _, [:erlang, :+]}, _, [1]}]} = + expand(quote(do: +1 = +1)) assert {:=, _, [[{:|, _, [1, [{:|, _, [2, 3]}]]}], [1, 2, 3]]} = expand(quote(do: [1] ++ [2] ++ 3 = [1, 2, 3])) diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs index d495ce9de8c..70e7ca108d6 100644 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ b/lib/elixir/test/elixir/module/types/pattern_test.exs @@ -15,6 +15,20 @@ defmodule Module.Types.PatternTest do end end + defmacrop quoted_pattern_with_diagnostics(patterns) do + {ast, diagnostics} = Code.with_diagnostics(fn -> expand_head(patterns, true) end) + + quote do + {patterns, true} = unquote(Macro.escape(ast)) + + result = + Pattern.of_pattern(patterns, new_stack(), new_context()) + |> lift_result() + + {result, unquote(Macro.escape(diagnostics))} + end + end + defmacrop quoted_head(patterns, guards \\ []) do quote do {patterns, guards} = unquote(Macro.escape(expand_head(patterns, guards))) @@ -76,8 +90,14 @@ defmodule Module.Types.PatternTest do assert quoted_pattern(false) == {:ok, {:atom, false}} assert quoted_pattern(:foo) == {:ok, {:atom, :foo}} assert quoted_pattern(0) == {:ok, :integer} - assert quoted_pattern(0.0) == {:ok, :float} + assert quoted_pattern(+0.0) == {:ok, :float} + assert quoted_pattern(-0.0) == {:ok, :float} assert quoted_pattern("foo") == {:ok, :binary} + + assert {{:ok, :float}, [diagnostic]} = quoted_pattern_with_diagnostics(0.0) + + assert diagnostic.message =~ + "pattern matching on 0.0 is equivalent to matching only on +0.0" end test "list" do diff --git a/lib/elixir/test/erlang/control_test.erl b/lib/elixir/test/erlang/control_test.erl index fbe853bd2d3..7f3337f7880 100644 --- a/lib/elixir/test/erlang/control_test.erl +++ b/lib/elixir/test/erlang/control_test.erl @@ -12,6 +12,12 @@ cond_line_test() -> {clause, 3, _, _, _}] } = to_erl("cond do\n 1 -> :ok\n 2 -> :ok\nend"). +float_match_test() -> + {'case', _, _, + [{clause, _, [{op, _, '+', {float, _, 0.0}}], [], [{atom, _, pos}]}, + {clause, _, [{op, _, '-', {float, _, 0.0}}], [], [{atom, _, neg}]}] + } = to_erl("case X do\n +0.0 -> :pos\n -0.0 -> :neg\nend"). + % Optimized optimized_if_test() ->