Browse Source

Intermediate changes

robot-piglet 1 year ago
parent
commit
dfbaba0293

+ 1 - 1
contrib/python/hypothesis/py3/.dist-info/METADATA

@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: hypothesis
-Version: 6.89.0
+Version: 6.90.0
 Summary: A library for property-based testing
 Home-page: https://hypothesis.works
 Author: David R. MacIver and Zac Hatfield-Dodds

+ 4 - 4
contrib/python/hypothesis/py3/hypothesis/core.py

@@ -610,10 +610,10 @@ def get_random_for_wrapped_test(test, wrapped_test):
 
 @attr.s
 class Stuff:
-    selfy = attr.ib(default=None)
-    args = attr.ib(factory=tuple)
-    kwargs = attr.ib(factory=dict)
-    given_kwargs = attr.ib(factory=dict)
+    selfy: Any = attr.ib(default=None)
+    args: tuple = attr.ib(factory=tuple)
+    kwargs: dict = attr.ib(factory=dict)
+    given_kwargs: dict = attr.ib(factory=dict)
 
 
 def process_arguments_to_given(wrapped_test, arguments, kwargs, given_kwargs, params):

+ 6 - 5
contrib/python/hypothesis/py3/hypothesis/extra/_array_helpers.py

@@ -13,7 +13,7 @@ from typing import NamedTuple, Optional, Tuple, Union
 
 from hypothesis import assume, strategies as st
 from hypothesis.errors import InvalidArgument
-from hypothesis.internal.conjecture import utils as cu
+from hypothesis.internal.conjecture.utils import _calc_p_continue
 from hypothesis.internal.coverage import check_function
 from hypothesis.internal.validation import check_type, check_valid_interval
 from hypothesis.strategies._internal.utils import defines_strategy
@@ -544,7 +544,7 @@ class MutuallyBroadcastableShapesStrategy(st.SearchStrategy):
                 if name not in dims:
                     dim = name.strip("?")
                     dims[dim] = data.draw(self.side_strat)
-                    if self.min_dims == 0 and not data.draw_bits(3):
+                    if self.min_dims == 0 and not data.draw_boolean(7 / 8):
                         dims[dim + "?"] = None
                     else:
                         dims[dim + "?"] = dims[dim]
@@ -562,6 +562,9 @@ class MutuallyBroadcastableShapesStrategy(st.SearchStrategy):
             assert len(use) == self.num_shapes
             assert all(isinstance(x, bool) for x in use)
 
+        _gap = self.max_dims - self.min_dims
+        p_keep_extending_shape = _calc_p_continue(desired_avg=_gap / 2, max_size=_gap)
+
         for dim_count in range(1, self.max_dims + 1):
             dim = dim_count - 1
 
@@ -596,9 +599,7 @@ class MutuallyBroadcastableShapesStrategy(st.SearchStrategy):
                 # shape-tuple even if it is no longer being added to.
                 # This helps to ensure more stable shrinking behavior.
                 if self.min_dims < dim_count:
-                    use[shape_id] &= cu.biased_coin(
-                        data, 1 - 1 / (1 + self.max_dims - dim)
-                    )
+                    use[shape_id] &= data.draw_boolean(p_keep_extending_shape)
 
                 if use[shape_id]:
                     shape.append(side)

+ 1 - 1
contrib/python/hypothesis/py3/hypothesis/extra/array_api.py

@@ -422,7 +422,7 @@ class ArrayStrategy(st.SearchStrategy):
             seen = set()
 
             while elements.more():
-                i = cu.integer_range(data, 0, self.array_size - 1)
+                i = data.draw_integer(0, self.array_size - 1)
                 if i in assigned:
                     elements.reject()
                     continue

+ 1 - 1
contrib/python/hypothesis/py3/hypothesis/extra/lark.py

@@ -158,7 +158,7 @@ class LarkStrategy(st.SearchStrategy):
             data.stop_example()
 
     def gen_ignore(self, data, draw_state):
-        if self.ignored_symbols and data.draw_bits(2) == 3:
+        if self.ignored_symbols and data.draw_boolean(1 / 4):
             emit = data.draw(st.sampled_from(self.ignored_symbols))
             self.draw_symbol(data, emit, draw_state)
 

+ 2 - 2
contrib/python/hypothesis/py3/hypothesis/extra/numpy.py

@@ -330,7 +330,7 @@ class ArrayStrategy(st.SearchStrategy):
             seen = set()
 
             while elements.more():
-                i = cu.integer_range(data, 0, self.array_size - 1)
+                i = data.draw_integer(0, self.array_size - 1)
                 if not needs_fill[i]:
                     elements.reject()
                     continue
@@ -756,7 +756,7 @@ def array_dtypes(
         field_names,
         st.tuples(field_names, field_names).filter(lambda ns: ns[0] != ns[1]),
     )
-    elements = st.tuples(name_titles, subtype_strategy)
+    elements: st.SearchStrategy[tuple] = st.tuples(name_titles, subtype_strategy)
     if allow_subarrays:
         elements |= st.tuples(
             name_titles, subtype_strategy, array_shapes(max_dims=2, max_side=2)

+ 601 - 7
contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py

@@ -8,19 +8,23 @@
 # v. 2.0. If a copy of the MPL was not distributed with this file, You can
 # obtain one at https://mozilla.org/MPL/2.0/.
 
+import math
 import time
 from collections import defaultdict
 from enum import IntEnum
 from random import Random
+from sys import float_info
 from typing import (
     TYPE_CHECKING,
     Any,
+    Callable,
     Dict,
     FrozenSet,
     Hashable,
     Iterable,
     Iterator,
     List,
+    Literal,
     NoReturn,
     Optional,
     Sequence,
@@ -33,9 +37,27 @@ from typing import (
 import attr
 
 from hypothesis.errors import Frozen, InvalidArgument, StopTest
-from hypothesis.internal.compat import int_from_bytes, int_to_bytes
+from hypothesis.internal.cache import LRUReusedCache
+from hypothesis.internal.compat import floor, int_from_bytes, int_to_bytes
+from hypothesis.internal.conjecture.floats import float_to_lex, lex_to_float
 from hypothesis.internal.conjecture.junkdrawer import IntList, uniform
-from hypothesis.internal.conjecture.utils import calc_label_from_name
+from hypothesis.internal.conjecture.utils import (
+    INT_SIZES,
+    INT_SIZES_SAMPLER,
+    Sampler,
+    calc_label_from_name,
+    many,
+)
+from hypothesis.internal.floats import (
+    SIGNALING_NAN,
+    SMALLEST_SUBNORMAL,
+    float_to_int,
+    make_float_clamper,
+    next_down,
+    next_up,
+    sign_aware_lte,
+)
+from hypothesis.internal.intervalsets import IntervalSet
 
 if TYPE_CHECKING:
     from typing_extensions import dataclass_transform
@@ -51,9 +73,17 @@ else:
         return wrapper
 
 
+ONE_BOUND_INTEGERS_LABEL = calc_label_from_name("trying a one-bound int allowing 0")
+INTEGER_RANGE_DRAW_LABEL = calc_label_from_name("another draw in integer_range()")
+BIASED_COIN_LABEL = calc_label_from_name("biased_coin()")
+BIASED_COIN_INNER_LABEL = calc_label_from_name("inside biased_coin()")
+
 TOP_LABEL = calc_label_from_name("top")
 DRAW_BYTES_LABEL = calc_label_from_name("draw_bytes() in ConjectureData")
-
+DRAW_FLOAT_LABEL = calc_label_from_name("drawing a float")
+FLOAT_STRATEGY_DO_DRAW_LABEL = calc_label_from_name(
+    "getting another float in FloatStrategy"
+)
 
 InterestingOrigin = Tuple[
     Type[BaseException], str, int, Tuple[Any, ...], Tuple[Tuple[Any, ...], ...]
@@ -100,6 +130,39 @@ def structural_coverage(label: int) -> StructuralCoverageTag:
         return STRUCTURAL_COVERAGE_CACHE.setdefault(label, StructuralCoverageTag(label))
 
 
+NASTY_FLOATS = sorted(
+    [
+        0.0,
+        0.5,
+        1.1,
+        1.5,
+        1.9,
+        1.0 / 3,
+        10e6,
+        10e-6,
+        1.175494351e-38,
+        next_up(0.0),
+        float_info.min,
+        float_info.max,
+        3.402823466e38,
+        9007199254740992,
+        1 - 10e-6,
+        2 + 10e-6,
+        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
+    + [SIGNALING_NAN],
+    key=float_to_lex,
+)
+NASTY_FLOATS = list(map(float, NASTY_FLOATS))
+NASTY_FLOATS.extend([-x for x in NASTY_FLOATS])
+
+FLOAT_INIT_LOGIC_CACHE = LRUReusedCache(4096)
+
+
 class Example:
     """Examples track the hierarchical structure of draws from the byte stream,
     within a single test run.
@@ -796,6 +859,475 @@ BYTE_MASKS = [(1 << n) - 1 for n in range(8)]
 BYTE_MASKS[0] = 255
 
 
+class PrimitiveProvider:
+    # This is the low-level interface which would also be implemented
+    # by e.g. CrossHair, by an Atheris-hypothesis integration, etc.
+    # We'd then build the structured tree handling, database and replay
+    # support, etc. on top of this - so all backends get those for free.
+    #
+    # See https://github.com/HypothesisWorks/hypothesis/issues/3086
+
+    def __init__(self, conjecturedata: "ConjectureData", /) -> None:
+        self._cd = conjecturedata
+
+    def draw_boolean(self, p: float = 0.5, *, forced: Optional[bool] = None) -> bool:
+        """Return True with probability p (assuming a uniform generator),
+        shrinking towards False. If ``forced`` is set to a non-None value, this
+        will always return that value but will write choices appropriate to having
+        drawn that value randomly."""
+        # Note that this could also be implemented in terms of draw_integer().
+
+        # NB this function is vastly more complicated than it may seem reasonable
+        # for it to be. This is because it is used in a lot of places and it's
+        # important for it to shrink well, so it's worth the engineering effort.
+
+        if p <= 0 or p >= 1:
+            bits = 1
+        else:
+            # When there is a meaningful draw, in order to shrink well we will
+            # set things up so that 0 and 1 always correspond to False and True
+            # respectively. This means we want enough bits available that in a
+            # draw we will always have at least one truthy value and one falsey
+            # value.
+            bits = math.ceil(-math.log(min(p, 1 - p), 2))
+        # In order to avoid stupidly large draws where the probability is
+        # effectively zero or one, we treat probabilities of under 2^-64 to be
+        # effectively zero.
+        if bits > 64:
+            # There isn't enough precision near one for this to occur for values
+            # far from 0.
+            p = 0.0
+            bits = 1
+
+        size = 2**bits
+
+        self._cd.start_example(BIASED_COIN_LABEL)
+        while True:
+            # The logic here is a bit complicated and special cased to make it
+            # play better with the shrinker.
+
+            # We imagine partitioning the real interval [0, 1] into 2**n equal parts
+            # and looking at each part and whether its interior is wholly <= p
+            # or wholly >= p. At most one part can be neither.
+
+            # We then pick a random part. If it's wholly on one side or the other
+            # of p then we use that as the answer. If p is contained in the
+            # interval then we start again with a new probability that is given
+            # by the fraction of that interval that was <= our previous p.
+
+            # We then take advantage of the fact that we have control of the
+            # labelling to make this shrink better, using the following tricks:
+
+            # If p is <= 0 or >= 1 the result of this coin is certain. We make sure
+            # to write a byte to the data stream anyway so that these don't cause
+            # difficulties when shrinking.
+            if p <= 0:
+                self._cd.draw_bits(1, forced=0)
+                result = False
+            elif p >= 1:
+                self._cd.draw_bits(1, forced=1)
+                result = True
+            else:
+                falsey = floor(size * (1 - p))
+                truthy = floor(size * p)
+                remainder = size * p - truthy
+
+                if falsey + truthy == size:
+                    partial = False
+                else:
+                    partial = True
+
+                if forced is None:
+                    # We want to get to the point where True is represented by
+                    # 1 and False is represented by 0 as quickly as possible, so
+                    # we use the remove_discarded machinery in the shrinker to
+                    # achieve that by discarding any draws that are > 1 and writing
+                    # a suitable draw into the choice sequence at the end of the
+                    # loop.
+                    self._cd.start_example(BIASED_COIN_INNER_LABEL)
+                    i = self._cd.draw_bits(bits)
+                    self._cd.stop_example(discard=i > 1)
+                else:
+                    i = self._cd.draw_bits(bits, forced=int(forced))
+
+                # We always choose the region that causes us to repeat the loop as
+                # the maximum value, so that shrinking the drawn bits never causes
+                # us to need to draw more self._cd.
+                if partial and i == size - 1:
+                    p = remainder
+                    continue
+                if falsey == 0:
+                    # Every other partition is truthy, so the result is true
+                    result = True
+                elif truthy == 0:
+                    # Every other partition is falsey, so the result is false
+                    result = False
+                elif i <= 1:
+                    # We special case so that zero is always false and 1 is always
+                    # true which makes shrinking easier because we can always
+                    # replace a truthy block with 1. This has the slightly weird
+                    # property that shrinking from 2 to 1 can cause the result to
+                    # grow, but the shrinker always tries 0 and 1 first anyway, so
+                    # this will usually be fine.
+                    result = bool(i)
+                else:
+                    # Originally everything in the region 0 <= i < falsey was false
+                    # and everything above was true. We swapped one truthy element
+                    # into this region, so the region becomes 0 <= i <= falsey
+                    # except for i = 1. We know i > 1 here, so the test for truth
+                    # becomes i > falsey.
+                    result = i > falsey
+
+                if i > 1:
+                    self._cd.draw_bits(bits, forced=int(result))
+            break
+        self._cd.stop_example()
+        return result
+
+    def draw_integer(
+        self,
+        min_value: Optional[int] = None,
+        max_value: Optional[int] = None,
+        *,
+        # weights are for choosing an element index from a bounded range
+        weights: Optional[Sequence[float]] = None,
+        shrink_towards: int = 0,
+        forced: Optional[int] = None,
+    ) -> int:
+        # This is easy to build on top of our existing conjecture utils,
+        # and it's easy to build sampled_from and weighted_coin on this.
+        if weights is not None:
+            assert min_value is not None
+            assert max_value is not None
+
+            sampler = Sampler(weights)
+            idx = sampler.sample(self._cd)
+
+            if shrink_towards <= min_value:
+                return min_value + idx
+            elif max_value <= shrink_towards:
+                return max_value - idx
+            else:
+                # For range -2..2, interpret idx = 0..4 as [0, 1, 2, -1, -2]
+                if idx <= (gap := max_value - shrink_towards):
+                    return shrink_towards + idx
+                else:
+                    return shrink_towards - (idx - gap)
+
+        if min_value is None and max_value is None:
+            return self._draw_unbounded_integer()
+
+        if min_value is None:
+            assert max_value is not None  # make mypy happy
+            if max_value <= shrink_towards:
+                return max_value - abs(self._draw_unbounded_integer())
+            else:
+                probe = max_value + 1
+                while max_value < probe:
+                    self._cd.start_example(ONE_BOUND_INTEGERS_LABEL)
+                    probe = self._draw_unbounded_integer() + shrink_towards
+                    self._cd.stop_example(discard=max_value < probe)
+                return probe
+
+        if max_value is None:
+            assert min_value is not None
+            if min_value >= shrink_towards:
+                return min_value + abs(self._draw_unbounded_integer())
+            else:
+                probe = min_value - 1
+                while probe < min_value:
+                    self._cd.start_example(ONE_BOUND_INTEGERS_LABEL)
+                    probe = self._draw_unbounded_integer() + shrink_towards
+                    self._cd.stop_example(discard=probe < min_value)
+                return probe
+
+        return self._draw_bounded_integer(
+            min_value,
+            max_value,
+            center=shrink_towards,
+            forced=forced,
+        )
+
+    def draw_float(
+        self,
+        *,
+        min_value: float = -math.inf,
+        max_value: float = math.inf,
+        allow_nan: bool = True,
+        smallest_nonzero_magnitude: float,
+        # TODO: consider supporting these float widths at the IR level in the
+        # future.
+        # width: Literal[16, 32, 64] = 64,
+        # exclude_min and exclude_max handled higher up
+    ) -> float:
+        (
+            sampler,
+            forced_sign_bit,
+            neg_clamper,
+            pos_clamper,
+            nasty_floats,
+        ) = self._draw_float_init_logic(
+            min_value=min_value,
+            max_value=max_value,
+            allow_nan=allow_nan,
+            smallest_nonzero_magnitude=smallest_nonzero_magnitude,
+        )
+
+        while True:
+            self._cd.start_example(FLOAT_STRATEGY_DO_DRAW_LABEL)
+            i = sampler.sample(self._cd) if sampler else 0
+            self._cd.start_example(DRAW_FLOAT_LABEL)
+            if i == 0:
+                result = self._draw_float(forced_sign_bit=forced_sign_bit)
+                if math.copysign(1.0, result) == -1:
+                    assert neg_clamper is not None
+                    clamped = -neg_clamper(-result)
+                else:
+                    assert pos_clamper is not None
+                    clamped = pos_clamper(result)
+                if clamped != result:
+                    self._cd.stop_example(discard=True)
+                    self._cd.start_example(DRAW_FLOAT_LABEL)
+                    self._write_float(clamped)
+                    result = clamped
+            else:
+                result = nasty_floats[i - 1]
+
+                self._write_float(result)
+
+            self._cd.stop_example()  # (DRAW_FLOAT_LABEL)
+            self._cd.stop_example()  # (FLOAT_STRATEGY_DO_DRAW_LABEL)
+            return result
+
+    def draw_string(
+        self,
+        intervals: IntervalSet,
+        *,
+        min_size: int = 0,
+        max_size: Optional[int] = None,
+    ) -> str:
+        if max_size is None:
+            max_size = 10**10  # "arbitrarily large"
+
+        average_size = min(
+            max(min_size * 2, min_size + 5),
+            0.5 * (min_size + max_size),
+        )
+
+        chars = []
+        elements = many(
+            self._cd,
+            min_size=min_size,
+            max_size=max_size,
+            average_size=average_size,
+        )
+        while elements.more():
+            if len(intervals) > 256:
+                if self.draw_boolean(0.2):
+                    i = self._draw_bounded_integer(256, len(intervals) - 1)
+                else:
+                    i = self._draw_bounded_integer(0, 255)
+            else:
+                i = self._draw_bounded_integer(0, len(intervals) - 1)
+
+            chars.append(intervals.char_in_shrink_order(i))
+
+        return "".join(chars)
+
+    def draw_bytes(self, size: int) -> bytes:
+        return self._cd.draw_bits(8 * size).to_bytes(size, "big")
+
+    def _draw_float(self, forced_sign_bit: Optional[int] = None) -> float:
+        """
+        Helper for draw_float which draws a random 64-bit float.
+        """
+        self._cd.start_example(DRAW_FLOAT_LABEL)
+        try:
+            is_negative = self._cd.draw_bits(1, forced=forced_sign_bit)
+            f = lex_to_float(self._cd.draw_bits(64))
+            return -f if is_negative else f
+        finally:
+            self._cd.stop_example()
+
+    def _write_float(self, f: float) -> None:
+        sign = float_to_int(f) >> 63
+        self._cd.draw_bits(1, forced=sign)
+        self._cd.draw_bits(64, forced=float_to_lex(abs(f)))
+
+    def _draw_unbounded_integer(self) -> int:
+        size = INT_SIZES[INT_SIZES_SAMPLER.sample(self._cd)]
+        r = self._cd.draw_bits(size)
+        sign = r & 1
+        r >>= 1
+        if sign:
+            r = -r
+        return int(r)
+
+    def _draw_bounded_integer(
+        self,
+        lower: int,
+        upper: int,
+        *,
+        center: Optional[int] = None,
+        forced: Optional[int] = None,
+    ) -> int:
+        assert lower <= upper
+        assert forced is None or lower <= forced <= upper
+        if lower == upper:
+            # Write a value even when this is trivial so that when a bound depends
+            # on other values we don't suddenly disappear when the gap shrinks to
+            # zero - if that happens then often the data stream becomes misaligned
+            # and we fail to shrink in cases where we really should be able to.
+            self._cd.draw_bits(1, forced=0)
+            return int(lower)
+
+        if center is None:
+            center = lower
+        center = min(max(center, lower), upper)
+
+        if center == upper:
+            above = False
+        elif center == lower:
+            above = True
+        else:
+            force_above = None if forced is None else forced < center
+            above = not self._cd.draw_bits(1, forced=force_above)
+
+        if above:
+            gap = upper - center
+        else:
+            gap = center - lower
+
+        assert gap > 0
+
+        bits = gap.bit_length()
+        probe = gap + 1
+
+        if bits > 24 and self._cd.draw_bits(3, forced=None if forced is None else 0):
+            # For large ranges, we combine the uniform random distribution from draw_bits
+            # with a weighting scheme with moderate chance.  Cutoff at 2 ** 24 so that our
+            # choice of unicode characters is uniform but the 32bit distribution is not.
+            idx = INT_SIZES_SAMPLER.sample(self._cd)
+            bits = min(bits, INT_SIZES[idx])
+
+        while probe > gap:
+            self._cd.start_example(INTEGER_RANGE_DRAW_LABEL)
+            probe = self._cd.draw_bits(
+                bits, forced=None if forced is None else abs(forced - center)
+            )
+            self._cd.stop_example(discard=probe > gap)
+
+        if above:
+            result = center + probe
+        else:
+            result = center - probe
+
+        assert lower <= result <= upper
+        assert forced is None or result == forced, (result, forced, center, above)
+        return result
+
+    @classmethod
+    def _draw_float_init_logic(
+        cls,
+        *,
+        min_value: float,
+        max_value: float,
+        allow_nan: bool,
+        smallest_nonzero_magnitude: float,
+    ) -> Tuple[
+        Optional[Sampler],
+        Optional[Literal[0, 1]],
+        Optional[Callable[[float], float]],
+        Optional[Callable[[float], float]],
+        List[float],
+    ]:
+        """
+        Caches initialization logic for draw_float, as an alternative to
+        computing this for *every* float draw.
+        """
+        # float_to_int allows us to distinguish between e.g. -0.0 and 0.0,
+        # even in light of hash(-0.0) == hash(0.0) and -0.0 == 0.0.
+        key = (
+            float_to_int(min_value),
+            float_to_int(max_value),
+            allow_nan,
+            float_to_int(smallest_nonzero_magnitude),
+        )
+        if key in FLOAT_INIT_LOGIC_CACHE:
+            return FLOAT_INIT_LOGIC_CACHE[key]
+
+        result = cls._compute_draw_float_init_logic(
+            min_value=min_value,
+            max_value=max_value,
+            allow_nan=allow_nan,
+            smallest_nonzero_magnitude=smallest_nonzero_magnitude,
+        )
+        FLOAT_INIT_LOGIC_CACHE[key] = result
+        return result
+
+    @staticmethod
+    def _compute_draw_float_init_logic(
+        *,
+        min_value: float,
+        max_value: float,
+        allow_nan: bool,
+        smallest_nonzero_magnitude: float,
+    ) -> Tuple[
+        Optional[Sampler],
+        Optional[Literal[0, 1]],
+        Optional[Callable[[float], float]],
+        Optional[Callable[[float], float]],
+        List[float],
+    ]:
+        if smallest_nonzero_magnitude == 0.0:  # pragma: no cover
+            raise FloatingPointError(
+                "Got allow_subnormal=True, but we can't represent subnormal floats "
+                "right now, in violation of the IEEE-754 floating-point "
+                "specification.  This is usually because something was compiled with "
+                "-ffast-math or a similar option, which sets global processor state.  "
+                "See https://simonbyrne.github.io/notes/fastmath/ for a more detailed "
+                "writeup - and good luck!"
+            )
+
+        def permitted(f):
+            assert isinstance(f, float)
+            if math.isnan(f):
+                return allow_nan
+            if 0 < abs(f) < smallest_nonzero_magnitude:
+                return False
+            return sign_aware_lte(min_value, f) and sign_aware_lte(f, max_value)
+
+        boundary_values = [
+            min_value,
+            next_up(min_value),
+            min_value + 1,
+            max_value - 1,
+            next_down(max_value),
+            max_value,
+        ]
+        nasty_floats = [f for f in NASTY_FLOATS + boundary_values if permitted(f)]
+        weights = [0.2 * len(nasty_floats)] + [0.8] * len(nasty_floats)
+        sampler = Sampler(weights) if nasty_floats else None
+
+        pos_clamper = neg_clamper = None
+        if sign_aware_lte(0.0, max_value):
+            pos_min = max(min_value, smallest_nonzero_magnitude)
+            allow_zero = sign_aware_lte(min_value, 0.0)
+            pos_clamper = make_float_clamper(pos_min, max_value, allow_zero=allow_zero)
+        if sign_aware_lte(min_value, -0.0):
+            neg_max = min(max_value, -smallest_nonzero_magnitude)
+            allow_zero = sign_aware_lte(-0.0, max_value)
+            neg_clamper = make_float_clamper(
+                -neg_max, -min_value, allow_zero=allow_zero
+            )
+
+        forced_sign_bit: Optional[Literal[0, 1]] = None
+        if (pos_clamper is None) != (neg_clamper is None):
+            forced_sign_bit = 1 if neg_clamper else 0
+
+        return (sampler, forced_sign_bit, neg_clamper, pos_clamper, nasty_floats)
+
+
 class ConjectureData:
     @classmethod
     def for_buffer(
@@ -841,6 +1373,7 @@ class ConjectureData:
         self.draw_times: "List[float]" = []
         self.max_depth = 0
         self.has_discards = False
+        self.provider = PrimitiveProvider(self)
 
         self.__result: "Optional[ConjectureResult]" = None
 
@@ -879,6 +1412,71 @@ class ConjectureData:
             ", frozen" if self.frozen else "",
         )
 
+    def draw_integer(
+        self,
+        min_value: Optional[int] = None,
+        max_value: Optional[int] = None,
+        *,
+        # weights are for choosing an element index from a bounded range
+        weights: Optional[Sequence[float]] = None,
+        shrink_towards: int = 0,
+        forced: Optional[int] = None,
+    ) -> int:
+        # Validate arguments
+        if weights is not None:
+            assert min_value is not None
+            assert max_value is not None
+            assert (max_value - min_value) <= 1024  # arbitrary practical limit
+
+        if forced is not None:
+            assert min_value is not None
+            assert max_value is not None
+
+        return self.provider.draw_integer(
+            min_value=min_value,
+            max_value=max_value,
+            weights=weights,
+            shrink_towards=shrink_towards,
+            forced=forced,
+        )
+
+    def draw_float(
+        self,
+        min_value: float = -math.inf,
+        max_value: float = math.inf,
+        *,
+        allow_nan: bool = True,
+        smallest_nonzero_magnitude: float = SMALLEST_SUBNORMAL,
+        # TODO: consider supporting these float widths at the IR level in the
+        # future.
+        # width: Literal[16, 32, 64] = 64,
+        # exclude_min and exclude_max handled higher up
+    ) -> float:
+        assert smallest_nonzero_magnitude > 0
+        return self.provider.draw_float(
+            min_value=min_value,
+            max_value=max_value,
+            allow_nan=allow_nan,
+            smallest_nonzero_magnitude=smallest_nonzero_magnitude,
+        )
+
+    def draw_string(
+        self,
+        intervals: IntervalSet,
+        *,
+        min_size: int = 0,
+        max_size: Optional[int] = None,
+    ) -> str:
+        return self.provider.draw_string(
+            intervals, min_size=min_size, max_size=max_size
+        )
+
+    def draw_bytes(self, size: int) -> bytes:
+        return self.provider.draw_bytes(size)
+
+    def draw_boolean(self, p: float = 0.5, *, forced: Optional[bool] = None) -> bool:
+        return self.provider.draw_boolean(p, forced=forced)
+
     def as_result(self) -> Union[ConjectureResult, _Overrun]:
         """Convert the result of running this test into
         either an Overrun object or a ConjectureResult."""
@@ -1099,10 +1697,6 @@ class ConjectureData:
         assert result.bit_length() <= n
         return result
 
-    def draw_bytes(self, n: int) -> bytes:
-        """Draw n bytes from the underlying source."""
-        return int_to_bytes(self.draw_bits(8 * n), n)
-
     def write(self, string: bytes) -> Optional[bytes]:
         """Write ``string`` to the output buffer."""
         self.__assert_not_frozen("write")

+ 0 - 23
contrib/python/hypothesis/py3/hypothesis/internal/conjecture/floats.py

@@ -9,14 +9,9 @@
 # obtain one at https://mozilla.org/MPL/2.0/.
 
 from array import array
-from typing import TYPE_CHECKING, Optional
 
-from hypothesis.internal.conjecture.utils import calc_label_from_name
 from hypothesis.internal.floats import float_to_int, int_to_float
 
-if TYPE_CHECKING:
-    from hypothesis.internal.conjecture.data import ConjectureData
-
 """
 This module implements support for arbitrary floating point numbers in
 Conjecture. It doesn't make any attempt to get a good distribution, only to
@@ -83,8 +78,6 @@ MAX_EXPONENT = 0x7FF
 BIAS = 1023
 MAX_POSITIVE_EXPONENT = MAX_EXPONENT - 1 - BIAS
 
-DRAW_FLOAT_LABEL = calc_label_from_name("drawing a float")
-
 
 def exponent_key(e: int) -> float:
     if e == MAX_EXPONENT:
@@ -224,19 +217,3 @@ def is_simple(f: float) -> int:
     if i != f:
         return False
     return i.bit_length() <= 56
-
-
-def draw_float(data: "ConjectureData", forced_sign_bit: Optional[int] = None) -> float:
-    try:
-        data.start_example(DRAW_FLOAT_LABEL)
-        is_negative = data.draw_bits(1, forced=forced_sign_bit)
-        f = lex_to_float(data.draw_bits(64))
-        return -f if is_negative else f
-    finally:
-        data.stop_example()
-
-
-def write_float(data: "ConjectureData", f: float) -> None:
-    sign = float_to_int(f) >> 63
-    data.draw_bits(1, forced=sign)
-    data.draw_bits(64, forced=float_to_lex(abs(f)))

+ 6 - 5
contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py

@@ -19,13 +19,14 @@ from hypothesis.internal.conjecture.choicetree import (
     prefix_selection_order,
     random_selection_order,
 )
-from hypothesis.internal.conjecture.data import ConjectureData, ConjectureResult, Status
-from hypothesis.internal.conjecture.dfa import ConcreteDFA
-from hypothesis.internal.conjecture.floats import (
+from hypothesis.internal.conjecture.data import (
     DRAW_FLOAT_LABEL,
-    float_to_lex,
-    lex_to_float,
+    ConjectureData,
+    ConjectureResult,
+    Status,
 )
+from hypothesis.internal.conjecture.dfa import ConcreteDFA
+from hypothesis.internal.conjecture.floats import float_to_lex, lex_to_float
 from hypothesis.internal.conjecture.junkdrawer import (
     binary_search,
     find_integer,

+ 6 - 207
contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py

@@ -11,15 +11,14 @@
 import enum
 import hashlib
 import heapq
-import math
 import sys
 from collections import OrderedDict, abc
 from functools import lru_cache
 from typing import TYPE_CHECKING, List, Optional, Sequence, Tuple, Type, TypeVar, Union
 
 from hypothesis.errors import InvalidArgument
-from hypothesis.internal.compat import floor, int_from_bytes
-from hypothesis.internal.floats import int_to_float, next_up
+from hypothesis.internal.compat import int_from_bytes
+from hypothesis.internal.floats import next_up
 
 if TYPE_CHECKING:
     from hypothesis.internal.conjecture.data import ConjectureData
@@ -45,86 +44,10 @@ def combine_labels(*labels: int) -> int:
     return label
 
 
-INTEGER_RANGE_DRAW_LABEL = calc_label_from_name("another draw in integer_range()")
-BIASED_COIN_LABEL = calc_label_from_name("biased_coin()")
-BIASED_COIN_INNER_LABEL = calc_label_from_name("inside biased_coin()")
 SAMPLE_IN_SAMPLER_LABEL = calc_label_from_name("a sample() in Sampler")
 ONE_FROM_MANY_LABEL = calc_label_from_name("one more from many()")
 
 
-def unbounded_integers(data: "ConjectureData") -> int:
-    size = INT_SIZES[INT_SIZES_SAMPLER.sample(data)]
-    r = data.draw_bits(size)
-    sign = r & 1
-    r >>= 1
-    if sign:
-        r = -r
-    return int(r)
-
-
-def integer_range(
-    data: "ConjectureData",
-    lower: int,
-    upper: int,
-    center: Optional[int] = None,
-    forced: Optional[int] = None,
-) -> int:
-    assert lower <= upper
-    assert forced is None or lower <= forced <= upper
-    if lower == upper:
-        # Write a value even when this is trivial so that when a bound depends
-        # on other values we don't suddenly disappear when the gap shrinks to
-        # zero - if that happens then often the data stream becomes misaligned
-        # and we fail to shrink in cases where we really should be able to.
-        data.draw_bits(1, forced=0)
-        return int(lower)
-
-    if center is None:
-        center = lower
-    center = min(max(center, lower), upper)
-
-    if center == upper:
-        above = False
-    elif center == lower:
-        above = True
-    else:
-        force_above = None if forced is None else forced < center
-        above = not data.draw_bits(1, forced=force_above)
-
-    if above:
-        gap = upper - center
-    else:
-        gap = center - lower
-
-    assert gap > 0
-
-    bits = gap.bit_length()
-    probe = gap + 1
-
-    if bits > 24 and data.draw_bits(3, forced=None if forced is None else 0):
-        # For large ranges, we combine the uniform random distribution from draw_bits
-        # with a weighting scheme with moderate chance.  Cutoff at 2 ** 24 so that our
-        # choice of unicode characters is uniform but the 32bit distribution is not.
-        idx = INT_SIZES_SAMPLER.sample(data)
-        bits = min(bits, INT_SIZES[idx])
-
-    while probe > gap:
-        data.start_example(INTEGER_RANGE_DRAW_LABEL)
-        probe = data.draw_bits(
-            bits, forced=None if forced is None else abs(forced - center)
-        )
-        data.stop_example(discard=probe > gap)
-
-    if above:
-        result = center + probe
-    else:
-        result = center - probe
-
-    assert lower <= result <= upper
-    assert forced is None or result == forced, (result, forced, center, above)
-    return result
-
-
 T = TypeVar("T")
 
 
@@ -159,131 +82,7 @@ def check_sample(
 
 
 def choice(data: "ConjectureData", values: Sequence[T]) -> T:
-    return values[integer_range(data, 0, len(values) - 1)]
-
-
-FLOAT_PREFIX = 0b1111111111 << 52
-FULL_FLOAT = int_to_float(FLOAT_PREFIX | ((2 << 53) - 1)) - 1
-
-
-def fractional_float(data: "ConjectureData") -> float:
-    return (int_to_float(FLOAT_PREFIX | data.draw_bits(52)) - 1) / FULL_FLOAT
-
-
-def biased_coin(
-    data: "ConjectureData", p: float, *, forced: Optional[bool] = None
-) -> bool:
-    """Return True with probability p (assuming a uniform generator),
-    shrinking towards False. If ``forced`` is set to a non-None value, this
-    will always return that value but will write choices appropriate to having
-    drawn that value randomly."""
-
-    # NB this function is vastly more complicated than it may seem reasonable
-    # for it to be. This is because it is used in a lot of places and it's
-    # important for it to shrink well, so it's worth the engineering effort.
-
-    if p <= 0 or p >= 1:
-        bits = 1
-    else:
-        # When there is a meaningful draw, in order to shrink well we will
-        # set things up so that 0 and 1 always correspond to False and True
-        # respectively. This means we want enough bits available that in a
-        # draw we will always have at least one truthy value and one falsey
-        # value.
-        bits = math.ceil(-math.log(min(p, 1 - p), 2))
-    # In order to avoid stupidly large draws where the probability is
-    # effectively zero or one, we treat probabilities of under 2^-64 to be
-    # effectively zero.
-    if bits > 64:
-        # There isn't enough precision near one for this to occur for values
-        # far from 0.
-        p = 0.0
-        bits = 1
-
-    size = 2**bits
-
-    data.start_example(BIASED_COIN_LABEL)
-    while True:
-        # The logic here is a bit complicated and special cased to make it
-        # play better with the shrinker.
-
-        # We imagine partitioning the real interval [0, 1] into 2**n equal parts
-        # and looking at each part and whether its interior is wholly <= p
-        # or wholly >= p. At most one part can be neither.
-
-        # We then pick a random part. If it's wholly on one side or the other
-        # of p then we use that as the answer. If p is contained in the
-        # interval then we start again with a new probability that is given
-        # by the fraction of that interval that was <= our previous p.
-
-        # We then take advantage of the fact that we have control of the
-        # labelling to make this shrink better, using the following tricks:
-
-        # If p is <= 0 or >= 1 the result of this coin is certain. We make sure
-        # to write a byte to the data stream anyway so that these don't cause
-        # difficulties when shrinking.
-        if p <= 0:
-            data.draw_bits(1, forced=0)
-            result = False
-        elif p >= 1:
-            data.draw_bits(1, forced=1)
-            result = True
-        else:
-            falsey = floor(size * (1 - p))
-            truthy = floor(size * p)
-            remainder = size * p - truthy
-
-            if falsey + truthy == size:
-                partial = False
-            else:
-                partial = True
-
-            if forced is None:
-                # We want to get to the point where True is represented by
-                # 1 and False is represented by 0 as quickly as possible, so
-                # we use the remove_discarded machinery in the shrinker to
-                # achieve that by discarding any draws that are > 1 and writing
-                # a suitable draw into the choice sequence at the end of the
-                # loop.
-                data.start_example(BIASED_COIN_INNER_LABEL)
-                i = data.draw_bits(bits)
-                data.stop_example(discard=i > 1)
-            else:
-                i = data.draw_bits(bits, forced=int(forced))
-
-            # We always choose the region that causes us to repeat the loop as
-            # the maximum value, so that shrinking the drawn bits never causes
-            # us to need to draw more data.
-            if partial and i == size - 1:
-                p = remainder
-                continue
-            if falsey == 0:
-                # Every other partition is truthy, so the result is true
-                result = True
-            elif truthy == 0:
-                # Every other partition is falsey, so the result is false
-                result = False
-            elif i <= 1:
-                # We special case so that zero is always false and 1 is always
-                # true which makes shrinking easier because we can always
-                # replace a truthy block with 1. This has the slightly weird
-                # property that shrinking from 2 to 1 can cause the result to
-                # grow, but the shrinker always tries 0 and 1 first anyway, so
-                # this will usually be fine.
-                result = bool(i)
-            else:
-                # Originally everything in the region 0 <= i < falsey was false
-                # and everything above was true. We swapped one truthy element
-                # into this region, so the region becomes 0 <= i <= falsey
-                # except for i = 1. We know i > 1 here, so the test for truth
-                # becomes i > falsey.
-                result = i > falsey
-
-            if i > 1:
-                data.draw_bits(bits, forced=int(result))
-        break
-    data.stop_example()
-    return result
+    return values[data.draw_integer(0, len(values) - 1)]
 
 
 class Sampler:
@@ -375,7 +174,7 @@ class Sampler:
     def sample(self, data: "ConjectureData") -> int:
         data.start_example(SAMPLE_IN_SAMPLER_LABEL)
         base, alternate, alternate_chance = choice(data, self.table)
-        use_alternate = biased_coin(data, alternate_chance)
+        use_alternate = data.draw_boolean(alternate_chance)
         data.stop_example()
         if use_alternate:
             return alternate
@@ -437,8 +236,8 @@ class many:
                 forced_result = True
             elif self.count >= self.max_size:
                 forced_result = False
-            should_continue = biased_coin(
-                self.data, self.p_continue, forced=forced_result
+            should_continue = self.data.draw_boolean(
+                self.p_continue, forced=forced_result
             )
 
         if should_continue:

Some files were not shown because too many files changed in this diff