File 2111-erts-Fix-term-equivalence-comparisons-on-floating-po.patch of Package erlang

From e3e6efef0592047da583ec62205e9d1805c9a153 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?John=20H=C3=B6gberg?= <john@erlang.org>
Date: Fri, 13 Oct 2023 09:52:32 +0200
Subject: [PATCH] erts: Fix term equivalence comparisons on floating-point zero

+0.0 will now be considered distinct from -0.0
---
 erts/emulator/beam/erl_term.h              |   4 +-
 erts/emulator/beam/erl_term_hashing.c      |   5 -
 erts/emulator/beam/erl_utils.h             |  29 +++++-
 erts/emulator/beam/utils.c                 |  22 ++++-
 erts/emulator/test/bs_match_misc_SUITE.erl |   2 +-
 erts/emulator/test/float_SUITE.erl         |   8 +-
 erts/emulator/test/hash_SUITE.erl          |   7 +-
 erts/emulator/test/num_bif_SUITE.erl       |   2 +-
 erts/emulator/test/op_SUITE.erl            | 105 +++++++++++++++++++--
 lib/compiler/src/beam_ssa_type.erl         |   5 +-
 lib/compiler/test/float_SUITE.erl          |   2 +-
 lib/stdlib/test/io_SUITE.erl               |   2 +-
 lib/stdlib/test/math_SUITE.erl             |   2 +-
 13 files changed, 160 insertions(+), 35 deletions(-)

diff --git a/erts/emulator/beam/erl_term.h b/erts/emulator/beam/erl_term.h
index b2432b6806..fcbd70809d 100644
--- a/erts/emulator/beam/erl_term.h
+++ b/erts/emulator/beam/erl_term.h
@@ -472,9 +472,7 @@ typedef union float_def
     byte   fb[sizeof(ieee754_8)];
     Uint16 fs[sizeof(ieee754_8) / sizeof(Uint16)];
     Uint32 fw[sizeof(ieee754_8) / sizeof(Uint32)];
-#if defined(ARCH_64)
-    Uint   fdw;
-#endif
+    Uint64 fdw;
 } FloatDef;
 
 #if defined(ARCH_64)
diff --git a/erts/emulator/beam/erl_term_hashing.c b/erts/emulator/beam/erl_term_hashing.c
index 7aa10c9a85..ef1c2f85b8 100644
--- a/erts/emulator/beam/erl_term_hashing.c
+++ b/erts/emulator/beam/erl_term_hashing.c
@@ -2108,11 +2108,6 @@ make_internal_hash(Eterm term, erts_ihash_t salt)
 
                 GET_DOUBLE(term, ff);
 
-                if (ff.fd == 0.0f) {
-                    /* ensure positive 0.0 */
-                    ff.fd = erts_get_positive_zero_float();
-                }
-
                 IHASH_MIX_ALPHA(IHASH_TYPE_FLOAT);
                 IHASH_MIX_BETA_2F32(ff.fw[0], ff.fw[1]);
 
diff --git a/erts/emulator/beam/erl_utils.h b/erts/emulator/beam/erl_utils.h
index dce3665a8d..b9b1d524eb 100644
--- a/erts/emulator/beam/erl_utils.h
+++ b/erts/emulator/beam/erl_utils.h
@@ -184,6 +184,8 @@ Sint erts_cmp_compound(Eterm, Eterm, int, int);
 
 #define erts_float_comp(x,y) (((x)<(y)) ? -1 : (((x)==(y)) ? 0 : 1))
 
+ERTS_GLB_INLINE Uint64 erts_float_exact_sortable(const FloatDef *value);
+
 #if ERTS_GLB_INLINE_INCL_FUNC_DEF
 
 ERTS_GLB_INLINE int erts_cmp_atoms(Eterm a, Eterm b) {
@@ -215,6 +217,22 @@ ERTS_GLB_INLINE int erts_cmp_atoms(Eterm a, Eterm b) {
     return len_a - len_b;
 }
 
+/* Provides a sortable integer from the raw bits of a floating-point number.
+ *
+ * When these are compared, they return the exact same result as a floating-
+ * point comparison of the inputs aside from the fact that -0.0 < +0.0, making
+ * it suitable for use in term equivalence operators and map key order. */
+ERTS_GLB_INLINE Uint64 erts_float_exact_sortable(const FloatDef *value) {
+    static const Uint64 sign_bit = ((Uint64)1) << 63;
+    Uint64 float_bits = value->fdw;
+
+    if (float_bits & sign_bit) {
+        return ~float_bits;
+    }
+
+    return float_bits ^ sign_bit;
+}
+
 ERTS_GLB_INLINE Sint erts_cmp(Eterm a, Eterm b, int exact, int eq_only) {
     if (is_atom(a) && is_atom(b)) {
         return erts_cmp_atoms(a, b);
@@ -226,7 +244,16 @@ ERTS_GLB_INLINE Sint erts_cmp(Eterm a, Eterm b, int exact, int eq_only) {
         GET_DOUBLE(a, af);
         GET_DOUBLE(b, bf);
 
-        return erts_float_comp(af.fd, bf.fd);
+        if (exact) {
+            Uint64 sortable_a, sortable_b;
+
+            sortable_a = erts_float_exact_sortable(&af);
+            sortable_b = erts_float_exact_sortable(&bf);
+
+            return erts_float_comp(sortable_a, sortable_b);
+        } else {
+            return erts_float_comp(af.fd, bf.fd);
+        }
     }
 
     return erts_cmp_compound(a,b,exact,eq_only);
diff --git a/erts/emulator/beam/utils.c b/erts/emulator/beam/utils.c
index 208675f865..286093c957 100644
--- a/erts/emulator/beam/utils.c
+++ b/erts/emulator/beam/utils.c
@@ -1403,7 +1403,9 @@ tailrecur_ne:
 		    if (is_float(b)) {
 			GET_DOUBLE(a, af);
 			GET_DOUBLE(b, bf);
-			if (af.fd == bf.fd) goto pop_next;
+                        if (af.fdw == bf.fdw) {
+                            goto pop_next;
+                        }
 		    }
 		    break; /* not equal */
 		}
@@ -2016,11 +2018,21 @@ tailrecur_ne:
 		    goto mixed_types;
 		} else {
 		    FloatDef af;
-		    FloatDef bf; 
+		    FloatDef bf;
+
+                    GET_DOUBLE(a, af);
+                    GET_DOUBLE(b, bf);
 
-		    GET_DOUBLE(a, af);
-		    GET_DOUBLE(b, bf);
-		    ON_CMP_GOTO(erts_float_comp(af.fd, bf.fd));
+                    if (exact) {
+                        Uint64 sortable_a, sortable_b;
+
+                        sortable_a = erts_float_exact_sortable(&af);
+                        sortable_b = erts_float_exact_sortable(&bf);
+
+                        ON_CMP_GOTO(erts_float_comp(sortable_a, sortable_b));
+                    } else {
+                        ON_CMP_GOTO(erts_float_comp(af.fd, bf.fd));
+                    }
 		}
 	    case (_TAG_HEADER_POS_BIG >> _TAG_PRIMARY_SIZE):
 	    case (_TAG_HEADER_NEG_BIG >> _TAG_PRIMARY_SIZE):
diff --git a/erts/emulator/test/bs_match_misc_SUITE.erl b/erts/emulator/test/bs_match_misc_SUITE.erl
index 83f4873dbe..284da3c3f3 100644
--- a/erts/emulator/test/bs_match_misc_SUITE.erl
+++ b/erts/emulator/test/bs_match_misc_SUITE.erl
@@ -91,7 +91,7 @@ float_middle_endian(Config) when is_list(Config) ->
     ok.
 
 
-fcmp(0.0, 0.0) -> ok;
+fcmp(F1, F2) when F1 == 0.0, F2 == 0.0 -> ok;
 fcmp(F1, F2) when (F1 - F2) / F2 < 0.0000001 -> ok.
     
 match_float(Bin0, Fsz, I) ->
diff --git a/erts/emulator/test/float_SUITE.erl b/erts/emulator/test/float_SUITE.erl
index e62f63db6f..9c4b01e35d 100644
--- a/erts/emulator/test/float_SUITE.erl
+++ b/erts/emulator/test/float_SUITE.erl
@@ -378,7 +378,7 @@ op_add(A, B) ->
     R = unify(catch A + B, Info),
     R = unify(my_apply(erlang,'+',[A,B]), Info),
     case R of
-        _ when A + B =:= element(1,R) -> ok;
+        _ when A + B == element(1,R) -> ok;
         {{'EXIT',badarith}, Info} -> ok
     end.
 
@@ -387,7 +387,7 @@ op_sub(A, B) ->
     R = unify(catch A - B, Info),
     R = unify(my_apply(erlang,'-',[A,B]), Info),
     case R of
-        _ when A - B =:= element(1,R) -> ok;
+        _ when A - B == element(1,R) -> ok;
         {{'EXIT',badarith}, Info} -> ok
     end.
 
@@ -396,7 +396,7 @@ op_mul(A, B) ->
     R = unify(catch A * B, Info),
     R = unify(my_apply(erlang,'*',[A,B]), Info),
     case R of
-        _ when A * B =:= element(1,R) -> ok;
+        _ when A * B == element(1,R) -> ok;
         {{'EXIT',badarith}, Info} -> ok
     end.
 
@@ -405,7 +405,7 @@ op_div(A, B) ->
     R = unify(catch A / B, Info),
     R = unify(my_apply(erlang,'/',[A,B]), Info),
     case R of
-        _ when A / B =:= element(1,R) -> ok;
+        _ when A / B == element(1,R) -> ok;
         {{'EXIT',badarith}, Info} -> ok
     end.
 
diff --git a/erts/emulator/test/hash_SUITE.erl b/erts/emulator/test/hash_SUITE.erl
index 90b96f2f97..e594c3b3be 100644
--- a/erts/emulator/test/hash_SUITE.erl
+++ b/erts/emulator/test/hash_SUITE.erl
@@ -835,9 +835,10 @@ hash_zero_test() ->
 hash_zero_test([Z|Zs],F) ->
     hash_zero_test(Zs,Z,F(Z),F).
 hash_zero_test([Z|Zs],Z0,V,F) ->
-    true = Z0 =:= Z, %% assert exact equal
-    Z0   = Z,        %% assert matching
-    V    = F(Z),     %% assert hash
+    true = (0.0 == Z0) andalso (0.0 == Z),
+    %% assert that phash and phash2 yield the same hash for both -0.0 and +0.0,
+    %% even though they are different terms since OTP 27.
+    V    = F(Z),
     hash_zero_test(Zs,Z0,V,F);
 hash_zero_test([],_,_,_) ->
     ok.
diff --git a/erts/emulator/test/num_bif_SUITE.erl b/erts/emulator/test/num_bif_SUITE.erl
index c53b660bf5..b986eb25b8 100644
--- a/erts/emulator/test/num_bif_SUITE.erl
+++ b/erts/emulator/test/num_bif_SUITE.erl
@@ -354,7 +354,7 @@ max_diff_decimals(F, D) ->
 
 t_string_to_float_safe(Config) when is_list(Config) ->
     test_stf(0.0,"0.0"),
-    test_stf(0.0,"-0.0"),
+    test_stf(-0.0,"-0.0"),
     test_stf(0.5,"0.5"),
     test_stf(-0.5,"-0.5"),
     test_stf(100.0,"1.0e2"),
diff --git a/erts/emulator/test/op_SUITE.erl b/erts/emulator/test/op_SUITE.erl
index 358c2e0df6..ef5bbccc27 100644
--- a/erts/emulator/test/op_SUITE.erl
+++ b/erts/emulator/test/op_SUITE.erl
@@ -25,7 +25,8 @@
 -export([all/0, suite/0,
          bsl_bsr/1,logical/1,t_not/1,relop_simple/1,relop/1,
          complex_relop/1,unsafe_fusing/1,
-         range_tests/1,combined_relops/1,typed_relop/1]).
+         range_tests/1,combined_relops/1,typed_relop/1,
+         term_equivalence/1]).
 
 -import(lists, [foldl/3,flatmap/2]).
 
@@ -36,7 +37,7 @@ suite() ->
 all() ->
     [bsl_bsr, logical, t_not, relop_simple, relop,
      complex_relop, unsafe_fusing, range_tests,
-     combined_relops, typed_relop].
+     combined_relops, typed_relop, term_equivalence].
 
 %% Test the bsl and bsr operators.
 bsl_bsr(Config) when is_list(Config) ->
@@ -146,8 +147,8 @@ relop_simple(Config) when is_list(Config) ->
     F2 = float(Big2),
     T1 = erlang:make_tuple(3,87),
     T2 = erlang:make_tuple(3,87),
-    Terms = [-F2,Big2,-F1,-Big1,-33,-33.0,0,0.0,42,42.0,Big1,F1,Big2,F2,a,b,
-             {T1,a},{T2,b},[T1,Big1],[T2,Big2]],
+    Terms = [-F2,Big2,-F1,-Big1,-33,-33.0,0,0.0,-0.0,42,42.0,Big1,F1,Big2,F2,
+             a,b,{T1,a},{T2,b},[T1,Big1],[T2,Big2]],
 
     Combos = [{V1,V2} || V1 <- Terms, V2 <- Terms],
 
@@ -181,6 +182,8 @@ relop_simple_do(V1,V2) ->
     ID = not (V1 =/= V2),
     ID = not (V2 =/= V1),
 
+    implies(ID, V1 == V2),
+
     EQ = V1 == V2,
     EQ = V2 == V1,
     EQ = not (V1 /= V2),
@@ -193,6 +196,9 @@ relop_simple_do(V1,V2) ->
         {false, false, false, true,  +1} -> ok
     end.
 
+implies(false, _) -> ok;
+implies(true, true) -> ok.
+
 %% Emulate internal "cmp"
 cmp_emu(A,B) when is_tuple(A), is_tuple(B) ->
     SA = size(A),
@@ -297,7 +303,7 @@ relop(Config) when is_list(Config) ->
     Bin = <<"abc">>,
     BitString = <<0:7>>,
     Map = #{a => b},
-    Vs0 = [a,b,-33,-33.0,0,0.0,42,42.0,Big1,Big2,F1,F2,
+    Vs0 = [a,b,-33,-33.0,0,0.0,-0,0,42,42.0,Big1,Big2,F1,F2,
            Bin,BitString,Map],
     Vs = [unvalue(V) || V <- Vs0],
     Ops = ['==', '/=', '=:=', '=/=', '<', '=<', '>', '>='],
@@ -310,7 +316,7 @@ complex_relop(Config) when is_list(Config) ->
     Bin = <<"abc">>,
     BitString = <<0:7>>,
     Map = #{a => b},
-    Vs0 = [an_atom,42.0,42,Big,Float,Bin,BitString,Map],
+    Vs0 = [an_atom,42.0,42,0.0,-0.0,Big,Float,Bin,BitString,Map],
     Vs = flatmap(fun(X) -> [unvalue({X}),unvalue([X])] end, Vs0),
     Ops = ['==', '/=', '=:=', '=/=', '<', '=<', '>', '>='],
     binop(Ops, Vs).
@@ -985,6 +991,93 @@ classify_value(N) when is_integer(N), N < 0 ->
 classify_value(_N) ->
     other.
 
+term_equivalence(_Config) ->
+    %% Term equivalence has been tested before in this suite, but we need to
+    %% massage some edge cases that cannot easily be put into the other test
+    %% cases.
+    <<PosZero/float>> = id(<<0:1,0:63>>),
+    <<NegZero/float>> = id(<<1:1,0:63>>),
+
+    +0.0 = id(PosZero),
+    -0.0 = id(NegZero),
+
+    -1 = erts_internal:cmp_term(NegZero, PosZero),
+    1 = erts_internal:cmp_term(PosZero, NegZero),
+
+    -1 = cmp_float(NegZero, PosZero),
+    1 = cmp_float(PosZero, NegZero),
+
+    Floats = [0.0, -0.0, 4711.0, -4711.0, 12.0, 943.0],
+
+    [true = (cmp_float(A, B) =:= erts_internal:cmp_term(A, B)) ||
+     A <- Floats, B <- Floats],
+
+    ok.
+
+cmp_float(A0, B0) ->
+    A = float_comparable(A0),
+    B = float_comparable(B0),
+    if
+        A < B -> -1;
+        A > B -> +1;
+        A =:= B -> 0
+    end.
+
+%% Converts a float to a number which, when compared with other such converted
+%% floats, is ordered the same as '<' on the original inputs aside from the
+%% fact that -0.0 < +0.0 as required by the term equivalence order.
+%%
+%% This has been proven correct with the SMT-LIB model below:
+%%
+%% (define-const SignBit_bv (_ BitVec 64) #x8000000000000000)
+%% 
+%% ; Two finite floats X and Y of unknown value
+%% (declare-const X (_ FloatingPoint 11 53))
+%% (declare-const Y (_ FloatingPoint 11 53))
+%% (assert (= false (fp.isInfinite X) (fp.isInfinite Y)
+%%            (fp.isNaN X) (fp.isNaN Y)))
+%% 
+%% ; ... the bit representations of the aforementioned floats. The Z3 floating-
+%% ; point extension lacks a way to directly bit-cast a vector to a float, so
+%% ; we rely on equivalence here.
+%% (declare-const X_bv (_ BitVec 64))
+%% (declare-const Y_bv (_ BitVec 64))
+%% (assert (= ((_ to_fp 11 53) X_bv) X))
+%% (assert (= ((_ to_fp 11 53) Y_bv) Y))
+%% 
+%% ; The bit hack we're going to test
+%% (define-fun float_sortable ((value (_ BitVec 64))) (_ BitVec 64)
+%% (ite (distinct (bvand value SignBit_bv) SignBit_bv)
+%%  (bvxor value SignBit_bv)
+%%  (bvnot value)))
+%% 
+%% (define-fun float_bv_lt ((LHS (_ BitVec 64))
+%%                          (RHS (_ BitVec 64))) Bool
+%%  (bvult (float_sortable LHS) (float_sortable RHS)))
+%% 
+%% (push 1)
+%%   ; When either of X or Y are non-zero, (X < Y) = (bvX < bvY)
+%%   (assert (not (and (fp.isZero X) (fp.isZero Y))))
+%%   (assert (distinct (fp.lt X Y) (float_bv_lt X_bv Y_bv)))
+%%   (check-sat) ; unsat, proving by negation that the above always holds
+%% (pop 1)
+%% 
+%% (push 1)
+%%   ; Negative zero should sort lower than positive zero
+%%   (assert (and (fp.isNegative X) (fp.isPositive Y)
+%%                (fp.isZero X) (fp.isZero Y)))
+%%   (assert (not (float_bv_lt X_bv Y_bv)))
+%%   (check-sat) ; unsat
+%% (pop 1)
+float_comparable(V0) ->
+    Sign = 16#8000000000000000,
+    Mask = 16#FFFFFFFFFFFFFFFF,
+    <<V_bv:64/unsigned>> = <<V0/float>>,
+    case V_bv band Sign of
+        0 -> (V_bv bxor Sign) band Mask;
+        Sign -> (bnot V_bv) band Mask
+    end.
+
 %%%
 %%% Utilities.
 %%%
diff --git a/lib/compiler/src/beam_ssa_type.erl b/lib/compiler/src/beam_ssa_type.erl
index 3db712ac01..1a988d2b13 100644
--- a/lib/compiler/src/beam_ssa_type.erl
+++ b/lib/compiler/src/beam_ssa_type.erl
@@ -1180,10 +1180,9 @@ simplify(#b_set{op={bif,Op0},args=[A,B]}=I, Ts, Ds) when Op0 =:= '==';
                {#t_integer{},#t_integer{}} ->
                    %% Both side contain integers but no floats.
                    true;
-               {#t_float{},#t_float{}} ->
-                   %% Both side contain floats but no integers.
-                   true;
                {_,_} ->
+                   %% Either side can contain a number, substitution is unsafe
+                   %% even if both sides are floats as `-0.0 == +0.0`
                    false
            end,
     case EqEq of
diff --git a/lib/compiler/test/float_SUITE.erl b/lib/compiler/test/float_SUITE.erl
index 7a5aedcec6..da2128a560 100644
--- a/lib/compiler/test/float_SUITE.erl
+++ b/lib/compiler/test/float_SUITE.erl
@@ -55,7 +55,7 @@ float_zero(Config) when is_list(Config) ->
     <<16#8000000000000000:64>> = match_on_zero_and_to_binary(-1*0.0),
     ok.
 
-match_on_zero_and_to_binary(0.0 = X) -> <<X/float>>.
+match_on_zero_and_to_binary(X) when X == 0.0 -> <<X/float>>.
 
 %% Thanks to Tobias Lindahl <tobias.lindahl@it.uu.se>
 %% Shows the effect of pending exceptions on the x86.
diff --git a/lib/stdlib/test/io_SUITE.erl b/lib/stdlib/test/io_SUITE.erl
index f26d98cc18..877e4eec75 100644
--- a/lib/stdlib/test/io_SUITE.erl
+++ b/lib/stdlib/test/io_SUITE.erl
@@ -1562,7 +1562,7 @@ f2r({S,BE,M}) when 0 =< S, S =< 1,
     <<F:64/float>> = <<S:1, BE:11, M:52>>,
     case catch T/N of
         {'EXIT', _} -> ok;
-        TN -> true = F =:= TN
+        TN -> true = F == TN
     end,
     Vr.
 
diff --git a/lib/stdlib/test/math_SUITE.erl b/lib/stdlib/test/math_SUITE.erl
index 5e92debd69..ba95aa350b 100644
--- a/lib/stdlib/test/math_SUITE.erl
+++ b/lib/stdlib/test/math_SUITE.erl
@@ -69,7 +69,7 @@ floor_ceil(_Config) ->
     MinusZero = 0.0/(-1.0),
     -43.0 = do_floor_ceil(-42.1),
     -43.0 = do_floor_ceil(-42.7),
-    0.0 = do_floor_ceil(MinusZero),
+    true = (0.0 == do_floor_ceil(MinusZero)),
     10.0 = do_floor_ceil(10.1),
     10.0 = do_floor_ceil(10.9),
 
-- 
2.35.3

openSUSE Build Service is sponsored by