Repository: HypothesisWorks/hypothesis

Issue:
`floats()` does not generate subnormal floating-point values
https://github.com/HypothesisWorks/hypothesis/blob/3226dd7d1b39d5ce38d559c0c4329aca40ecc9cb/hypothesis-python/tests/nocover/test_floating.py#L107-L136

This was discovered in #2973, but out of scope for that PR.  The first step is to determine when, *if ever*, this did work; what (if anything) changed to break it, and then to get subnormal-float generation working.


Discussion:
OK, on investigation it *is* strictly speaking possible for us to generate subnormal values; it's just that we do so with negligible probability because they only occur for one-in-2^11 possible exponents.  The obvious workaround is to just add a handful of special values to our `NASTY_FLOATS` table...

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py b/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
--- a/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
@@ -17,6 +17,7 @@
 import operator
 from decimal import Decimal
 from fractions import Fraction
+from sys import float_info
 from typing import Optional, Union
 
 from hypothesis.control import assume, reject
@@ -157,8 +158,9 @@ def integers(
         10e6,
         10e-6,
         1.175494351e-38,
-        2.2250738585072014e-308,
-        1.7976931348623157e308,
+        next_up(0.0),
+        float_info.min,
+        float_info.max,
         3.402823466e38,
         9007199254740992,
         1 - 10e-6,
@@ -166,6 +168,8 @@ def integers(
         1.192092896e-07,
         2.2204460492503131e-016,
     ]
+    + [2.0 ** -n for n in (24, 14, 149, 126)]  # minimum (sub)normals for float16,32
+    + [float_info.min / n for n in (2, 10, 1000, 100_000)]  # subnormal in float64
     + [math.inf, math.nan] * 5,
     key=flt.float_to_lex,
 )


Test patch:
diff --git a/hypothesis-python/tests/nocover/test_floating.py b/hypothesis-python/tests/nocover/test_floating.py
--- a/hypothesis-python/tests/nocover/test_floating.py
+++ b/hypothesis-python/tests/nocover/test_floating.py
@@ -21,6 +21,7 @@
 import pytest
 
 from hypothesis import HealthCheck, assume, given, settings
+from hypothesis.internal.floats import next_down
 from hypothesis.strategies import data, floats, lists
 
 from tests.common.utils import fails
@@ -105,35 +106,34 @@ def test_is_in_exact_int_range(x):
 
 
 # Tests whether we can represent subnormal floating point numbers.
-# This is essentially a function of how the python interpreter
-# was compiled.
+# IEE-754 requires subnormal support, but it's often disabled anyway by unsafe
+# compiler options like `-ffast-math`.  On most hardware that's even a global
+# config option, so *linking against* something built this way can break us.
 # Everything is terrible
-if math.ldexp(0.25, -1022) > 0:
-    REALLY_SMALL_FLOAT = sys.float_info.min
-else:
-    REALLY_SMALL_FLOAT = sys.float_info.min * 2
+FLUSH_SUBNORMALS_TO_ZERO = next_down(sys.float_info.min) == 0.0
 
 
-# These two tests have been failing for an unknown amount of time, but that
-# failure was previously being masked by a bug in our `@fails` decorator.
+def test_compiled_with_sane_math_options():
+    # Checks that we're not unexpectedly skipping the subnormal tests below.
+    assert not FLUSH_SUBNORMALS_TO_ZERO
 
 
-@pytest.mark.xfail
+@pytest.mark.skipif(FLUSH_SUBNORMALS_TO_ZERO, reason="broken by unsafe compiler flags")
 @fails
 @given(floats())
 @TRY_HARDER
 def test_can_generate_really_small_positive_floats(x):
     assume(x > 0)
-    assert x >= REALLY_SMALL_FLOAT
+    assert x >= sys.float_info.min
 
 
-@pytest.mark.xfail
+@pytest.mark.skipif(FLUSH_SUBNORMALS_TO_ZERO, reason="broken by unsafe compiler flags")
 @fails
 @given(floats())
 @TRY_HARDER
 def test_can_generate_really_small_negative_floats(x):
     assume(x < 0)
-    assert x <= -REALLY_SMALL_FLOAT
+    assert x <= -sys.float_info.min
 
 
 @fails