From d6021e4fa0e51683c4462489e5e27b91016ade9c Mon Sep 17 00:00:00 2001 From: sabiwara Date: Fri, 23 Feb 2024 21:33:45 +0900 Subject: [PATCH 1/2] Update float tests to catch -0.0 --- .formatter.exs | 5 +- lib/elixir/test/elixir/float_test.exs | 155 ++++++++++++++------------ 2 files changed, 87 insertions(+), 73 deletions(-) diff --git a/.formatter.exs b/.formatter.exs index f9bb2b1f0f2..b09e896884e 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -13,7 +13,10 @@ assert_same: 2, # Errors tests - assert_eval_raise: 3 + assert_eval_raise: 3, + + # Float tests + float_assert: 1 ], normalize_bitstring_modifiers: false ] diff --git a/lib/elixir/test/elixir/float_test.exs b/lib/elixir/test/elixir/float_test.exs index e56c3b991ae..cc4c96c0359 100644 --- a/lib/elixir/test/elixir/float_test.exs +++ b/lib/elixir/test/elixir/float_test.exs @@ -5,6 +5,17 @@ defmodule FloatTest do doctest Float + # TODO remove and replace by assert once we require Erlang/OTP 27+ + # We can't easily distinguish between -0.0 and +0.0 on previous version + defmacrop float_assert({:===, _, [left, right]}) do + quote do + # note: these are pure functions so no need to use bind_quoted + # we favor a useful error message instead + assert unquote(left) === unquote(right) + assert to_string(unquote(left)) === to_string(unquote(right)) + end + end + test "parse/1" do assert Float.parse("12") === {12.0, ""} assert Float.parse("-12") === {-12.0, ""} @@ -45,39 +56,39 @@ defmodule FloatTest do end test "floor/1" do - assert Float.floor(12.524235) === 12.0 - assert Float.floor(-12.5) === -13.0 - assert Float.floor(-12.524235) === -13.0 - assert Float.floor(7.5e3) === 7500.0 - assert Float.floor(7.5432e3) === 7543.0 - assert Float.floor(7.5e-3) === 0.0 - assert Float.floor(-12.32453e4) === -123_246.0 - assert Float.floor(-12.32453e-10) === -1.0 - assert Float.floor(0.32453e-10) === 0.0 - assert Float.floor(-0.32453e-10) === -1.0 - assert Float.floor(1.32453e-10) === 0.0 + float_assert Float.floor(12.524235) === 12.0 + float_assert Float.floor(-12.5) === -13.0 + float_assert Float.floor(-12.524235) === -13.0 + float_assert Float.floor(7.5e3) === 7500.0 + float_assert Float.floor(7.5432e3) === 7543.0 + float_assert Float.floor(7.5e-3) === 0.0 + float_assert Float.floor(-12.32453e4) === -123_246.0 + float_assert Float.floor(-12.32453e-10) === -1.0 + float_assert Float.floor(0.32453e-10) === 0.0 + float_assert Float.floor(-0.32453e-10) === -1.0 + float_assert Float.floor(1.32453e-10) === 0.0 end describe "floor/2" do test "with 0.0" do for precision <- 0..15 do - assert Float.floor(0.0, precision) === 0.0 - assert Float.floor(-0.0, precision) === -0.0 + float_assert Float.floor(0.0, precision) === 0.0 + float_assert Float.floor(-0.0, precision) === -0.0 end end test "floor/2 with precision" do - assert Float.floor(12.524235, 0) === 12.0 - assert Float.floor(-12.524235, 0) === -13.0 + float_assert Float.floor(12.524235, 0) === 12.0 + float_assert Float.floor(-12.524235, 0) === -13.0 - assert Float.floor(12.52, 2) === 12.51 - assert Float.floor(-12.52, 2) === -12.52 + float_assert Float.floor(12.52, 2) === 12.51 + float_assert Float.floor(-12.52, 2) === -12.52 - assert Float.floor(12.524235, 2) === 12.52 - assert Float.floor(-12.524235, 3) === -12.525 + float_assert Float.floor(12.524235, 2) === 12.52 + float_assert Float.floor(-12.524235, 3) === -12.525 - assert Float.floor(12.32453e-20, 2) === 0.0 - assert Float.floor(-12.32453e-20, 2) === -0.01 + float_assert Float.floor(12.32453e-20, 2) === 0.0 + float_assert Float.floor(-12.32453e-20, 2) === -0.01 assert_raise ArgumentError, "precision 16 is out of valid range of 0..15", fn -> Float.floor(1.1, 16) @@ -85,54 +96,54 @@ defmodule FloatTest do end test "with subnormal floats" do - assert Float.floor(-5.0e-324, 0) === -1.0 - assert Float.floor(-5.0e-324, 1) === -0.1 - assert Float.floor(-5.0e-324, 2) === -0.01 - assert Float.floor(-5.0e-324, 15) === -0.000000000000001 + float_assert Float.floor(-5.0e-324, 0) === -1.0 + float_assert Float.floor(-5.0e-324, 1) === -0.1 + float_assert Float.floor(-5.0e-324, 2) === -0.01 + float_assert Float.floor(-5.0e-324, 15) === -0.000000000000001 for precision <- 0..15 do - assert Float.floor(5.0e-324, precision) === 0.0 + float_assert Float.floor(5.0e-324, precision) === 0.0 end end end test "ceil/1" do - assert Float.ceil(12.524235) === 13.0 - assert Float.ceil(-12.5) === -12.0 - assert Float.ceil(-12.524235) === -12.0 - assert Float.ceil(7.5e3) === 7500.0 - assert Float.ceil(7.5432e3) === 7544.0 - assert Float.ceil(7.5e-3) === 1.0 - assert Float.ceil(-12.32453e4) === -123_245.0 - assert Float.ceil(-12.32453e-10) === -0.0 - assert Float.ceil(0.32453e-10) === 1.0 - assert Float.ceil(-0.32453e-10) === -0.0 - assert Float.ceil(1.32453e-10) === 1.0 - assert Float.ceil(0.0) === 0.0 + float_assert Float.ceil(12.524235) === 13.0 + float_assert Float.ceil(-12.5) === -12.0 + float_assert Float.ceil(-12.524235) === -12.0 + float_assert Float.ceil(7.5e3) === 7500.0 + float_assert Float.ceil(7.5432e3) === 7544.0 + float_assert Float.ceil(7.5e-3) === 1.0 + float_assert Float.ceil(-12.32453e4) === -123_245.0 + float_assert Float.ceil(-12.32453e-10) === -0.0 + float_assert Float.ceil(0.32453e-10) === 1.0 + float_assert Float.ceil(-0.32453e-10) === -0.0 + float_assert Float.ceil(1.32453e-10) === 1.0 + float_assert Float.ceil(0.0) === 0.0 end describe "ceil/2" do test "with 0.0" do for precision <- 0..15 do - assert Float.ceil(0.0, precision) === 0.0 - assert Float.ceil(-0.0, precision) === -0.0 + float_assert Float.ceil(0.0, precision) === 0.0 + float_assert Float.ceil(-0.0, precision) === -0.0 end end test "with regular floats" do - assert Float.ceil(12.524235, 0) === 13.0 - assert Float.ceil(-12.524235, 0) === -12.0 + float_assert Float.ceil(12.524235, 0) === 13.0 + float_assert Float.ceil(-12.524235, 0) === -12.0 - assert Float.ceil(12.52, 2) === 12.52 - assert Float.ceil(-12.52, 2) === -12.51 + float_assert Float.ceil(12.52, 2) === 12.52 + float_assert Float.ceil(-12.52, 2) === -12.51 - assert Float.ceil(12.524235, 2) === 12.53 - assert Float.ceil(-12.524235, 3) === -12.524 + float_assert Float.ceil(12.524235, 2) === 12.53 + float_assert Float.ceil(-12.524235, 3) === -12.524 - assert Float.ceil(12.32453e-20, 2) === 0.01 - assert Float.ceil(-12.32453e-20, 2) === -0.0 + float_assert Float.ceil(12.32453e-20, 2) === 0.01 + float_assert Float.ceil(-12.32453e-20, 2) === -0.0 - assert Float.ceil(0.0, 2) === 0.0 + float_assert Float.ceil(0.0, 2) === 0.0 assert_raise ArgumentError, "precision 16 is out of valid range of 0..15", fn -> Float.ceil(1.1, 16) @@ -140,18 +151,18 @@ defmodule FloatTest do end test "with small floats rounded up to -0.0" do - assert Float.ceil(-0.1, 0) === -0.0 - assert Float.ceil(-0.01, 1) === -0.0 + float_assert Float.ceil(-0.1, 0) === -0.0 + float_assert Float.ceil(-0.01, 1) === -0.0 end test "with subnormal floats" do - assert Float.ceil(5.0e-324, 0) === 1.0 - assert Float.ceil(5.0e-324, 1) === 0.1 - assert Float.ceil(5.0e-324, 2) === 0.01 - assert Float.ceil(5.0e-324, 15) === 0.000000000000001 + float_assert Float.ceil(5.0e-324, 0) === 1.0 + float_assert Float.ceil(5.0e-324, 1) === 0.1 + float_assert Float.ceil(5.0e-324, 2) === 0.01 + float_assert Float.ceil(5.0e-324, 15) === 0.000000000000001 for precision <- 0..15 do - assert Float.ceil(-5.0e-324, precision) === -0.0 + float_assert Float.ceil(-5.0e-324, precision) === -0.0 end end end @@ -159,18 +170,18 @@ defmodule FloatTest do describe "round/2" do test "with 0.0" do for precision <- 0..15 do - assert Float.round(0.0, precision) === 0.0 - assert Float.round(-0.0, precision) === -0.0 + float_assert Float.round(0.0, precision) === 0.0 + float_assert Float.round(-0.0, precision) === -0.0 end end test "with regular floats" do - assert Float.round(5.5675, 3) === 5.567 - assert Float.round(-5.5674, 3) === -5.567 - assert Float.round(5.5, 3) === 5.5 - assert Float.round(5.5e-10, 10) === 5.0e-10 - assert Float.round(5.5e-10, 8) === 0.0 - assert Float.round(5.0, 0) === 5.0 + float_assert Float.round(5.5675, 3) === 5.567 + float_assert Float.round(-5.5674, 3) === -5.567 + float_assert Float.round(5.5, 3) === 5.5 + float_assert Float.round(5.5e-10, 10) === 5.0e-10 + float_assert Float.round(5.5e-10, 8) === 0.0 + float_assert Float.round(5.0, 0) === 5.0 assert_raise ArgumentError, "precision 16 is out of valid range of 0..15", fn -> Float.round(1.1, 16) @@ -178,20 +189,20 @@ defmodule FloatTest do end test "with small floats rounded to +0.0 / -0.0" do - assert Float.round(0.01, 0) === 0.0 - assert Float.round(0.01, 1) === 0.0 + float_assert Float.round(0.01, 0) === 0.0 + float_assert Float.round(0.01, 1) === 0.0 - assert Float.round(-0.01, 0) === -0.0 - assert Float.round(-0.01, 1) === -0.0 + float_assert Float.round(-0.01, 0) === -0.0 + float_assert Float.round(-0.01, 1) === -0.0 - assert Float.round(-0.49999, 0) === -0.0 - assert Float.round(-0.049999, 1) === -0.0 + float_assert Float.round(-0.49999, 0) === -0.0 + float_assert Float.round(-0.049999, 1) === -0.0 end test "with subnormal floats" do for precision <- 0..15 do - assert Float.round(5.0e-324, precision) === 0.0 - assert Float.round(-5.0e-324, precision) === -0.0 + float_assert Float.round(5.0e-324, precision) === 0.0 + float_assert Float.round(-5.0e-324, precision) === -0.0 end end end From e7d3de0c91f8c948b3ffe9fc0556257068b49aa6 Mon Sep 17 00:00:00 2001 From: sabiwara Date: Fri, 23 Feb 2024 21:57:59 +0900 Subject: [PATCH 2/2] Fix unexpected rounding signs on OPT26- --- lib/elixir/lib/float.ex | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/float.ex b/lib/elixir/lib/float.ex index 7810d21b3bf..d837b13b013 100644 --- a/lib/elixir/lib/float.ex +++ b/lib/elixir/lib/float.ex @@ -374,8 +374,8 @@ defmodule Float do case rounding do :ceil when sign === 0 -> 1 / power_of_10(precision) :floor when sign === 1 -> -1 / power_of_10(precision) - :ceil when sign === 1 -> -0.0 - :half_up when sign === 1 -> -0.0 + :ceil when sign === 1 -> minus_zero() + :half_up when sign === 1 -> minus_zero() _ -> 0.0 end @@ -406,7 +406,7 @@ defmodule Float do cond do num == 0 and sign == 1 -> - -0.0 + minus_zero() num == 0 -> 0.0 @@ -422,6 +422,11 @@ defmodule Float do end end + # TODO remove once we require Erlang/OTP 27+ + # This function tricks the compiler to avoid this bug in previous versions: + # https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/float.ex#L408-L412 + defp minus_zero, do: -0.0 + defp decompose(significant, initial) do decompose(significant, 1, 0, initial) end