1# This file is part of Hypothesis, which may be found at
2# https://github.com/HypothesisWorks/hypothesis/
3#
4# Copyright the Hypothesis Authors.
5# Individual contributors are listed in AUTHORS.rst and the git log.
6#
7# This Source Code Form is subject to the terms of the Mozilla Public License,
8# v. 2.0. If a copy of the MPL was not distributed with this file, You can
9# obtain one at https://mozilla.org/MPL/2.0/.
10
11import math
12from decimal import Decimal
13from fractions import Fraction
14from typing import Literal, Optional, Union
15
16from hypothesis.control import reject
17from hypothesis.errors import InvalidArgument
18from hypothesis.internal.conjecture.data import ConjectureData
19from hypothesis.internal.filtering import (
20 get_float_predicate_bounds,
21 get_integer_predicate_bounds,
22)
23from hypothesis.internal.floats import (
24 SMALLEST_SUBNORMAL,
25 float_of,
26 float_to_int,
27 int_to_float,
28 is_negative,
29 next_down,
30 next_down_normal,
31 next_up,
32 next_up_normal,
33 width_smallest_normals,
34)
35from hypothesis.internal.validation import (
36 check_type,
37 check_valid_bound,
38 check_valid_interval,
39)
40from hypothesis.strategies._internal.misc import nothing
41from hypothesis.strategies._internal.strategies import (
42 SampledFromStrategy,
43 SearchStrategy,
44)
45from hypothesis.strategies._internal.utils import cacheable, defines_strategy
46
47# See https://github.com/python/mypy/issues/3186 - numbers.Real is wrong!
48Real = Union[int, float, Fraction, Decimal]
49
50
51class IntegersStrategy(SearchStrategy[int]):
52 def __init__(self, start: Optional[int], end: Optional[int]) -> None:
53 super().__init__()
54 assert isinstance(start, int) or start is None
55 assert isinstance(end, int) or end is None
56 assert start is None or end is None or start <= end
57 self.start = start
58 self.end = end
59
60 def __repr__(self) -> str:
61 if self.start is None and self.end is None:
62 return "integers()"
63 if self.end is None:
64 return f"integers(min_value={self.start})"
65 if self.start is None:
66 return f"integers(max_value={self.end})"
67 return f"integers({self.start}, {self.end})"
68
69 def do_draw(self, data: ConjectureData) -> int:
70 # For bounded integers, make the bounds and near-bounds more likely.
71 weights = None
72 if (
73 self.end is not None
74 and self.start is not None
75 and self.end - self.start > 127
76 ):
77 weights = {
78 self.start: (2 / 128),
79 self.start + 1: (1 / 128),
80 self.end - 1: (1 / 128),
81 self.end: (2 / 128),
82 }
83
84 return data.draw_integer(
85 min_value=self.start, max_value=self.end, weights=weights
86 )
87
88 def filter(self, condition):
89 if condition is math.isfinite:
90 return self
91 if condition in [math.isinf, math.isnan]:
92 return nothing()
93 constraints, pred = get_integer_predicate_bounds(condition)
94
95 start, end = self.start, self.end
96 if "min_value" in constraints:
97 start = max(constraints["min_value"], -math.inf if start is None else start)
98 if "max_value" in constraints:
99 end = min(constraints["max_value"], math.inf if end is None else end)
100
101 if start != self.start or end != self.end:
102 if start is not None and end is not None and start > end:
103 return nothing()
104 self = type(self)(start, end)
105 if pred is None:
106 return self
107 return super().filter(pred)
108
109
110@cacheable
111@defines_strategy(force_reusable_values=True)
112def integers(
113 min_value: Optional[int] = None,
114 max_value: Optional[int] = None,
115) -> SearchStrategy[int]:
116 """Returns a strategy which generates integers.
117
118 If min_value is not None then all values will be >= min_value. If
119 max_value is not None then all values will be <= max_value
120
121 Examples from this strategy will shrink towards zero, and negative values
122 will also shrink towards positive (i.e. -n may be replaced by +n).
123 """
124 check_valid_bound(min_value, "min_value")
125 check_valid_bound(max_value, "max_value")
126 check_valid_interval(min_value, max_value, "min_value", "max_value")
127
128 if min_value is not None:
129 if min_value != int(min_value):
130 raise InvalidArgument(
131 f"min_value={min_value!r} of type {type(min_value)!r} "
132 "cannot be exactly represented as an integer."
133 )
134 min_value = int(min_value)
135 if max_value is not None:
136 if max_value != int(max_value):
137 raise InvalidArgument(
138 f"max_value={max_value!r} of type {type(max_value)!r} "
139 "cannot be exactly represented as an integer."
140 )
141 max_value = int(max_value)
142
143 return IntegersStrategy(min_value, max_value)
144
145
146class FloatStrategy(SearchStrategy[float]):
147 """A strategy for floating point numbers."""
148
149 def __init__(
150 self,
151 *,
152 min_value: float,
153 max_value: float,
154 allow_nan: bool,
155 # The smallest nonzero number we can represent is usually a subnormal, but may
156 # be the smallest normal if we're running in unsafe denormals-are-zero mode.
157 # While that's usually an explicit error, we do need to handle the case where
158 # the user passes allow_subnormal=False.
159 smallest_nonzero_magnitude: float = SMALLEST_SUBNORMAL,
160 ):
161 super().__init__()
162 assert isinstance(allow_nan, bool)
163 assert smallest_nonzero_magnitude >= 0.0, "programmer error if this is negative"
164 if smallest_nonzero_magnitude == 0.0: # pragma: no cover
165 raise FloatingPointError(
166 "Got allow_subnormal=True, but we can't represent subnormal floats "
167 "right now, in violation of the IEEE-754 floating-point "
168 "specification. This is usually because something was compiled with "
169 "-ffast-math or a similar option, which sets global processor state. "
170 "See https://simonbyrne.github.io/notes/fastmath/ for a more detailed "
171 "writeup - and good luck!"
172 )
173 self.min_value = min_value
174 self.max_value = max_value
175 self.allow_nan = allow_nan
176 self.smallest_nonzero_magnitude = smallest_nonzero_magnitude
177
178 def __repr__(self) -> str:
179 return (
180 f"{self.__class__.__name__}({self.min_value=}, {self.max_value=}, "
181 f"{self.allow_nan=}, {self.smallest_nonzero_magnitude=})"
182 ).replace("self.", "")
183
184 def do_draw(self, data: ConjectureData) -> float:
185 return data.draw_float(
186 min_value=self.min_value,
187 max_value=self.max_value,
188 allow_nan=self.allow_nan,
189 smallest_nonzero_magnitude=self.smallest_nonzero_magnitude,
190 )
191
192 def filter(self, condition):
193 # Handle a few specific weird cases.
194 if condition is math.isfinite:
195 return FloatStrategy(
196 min_value=max(self.min_value, next_up(float("-inf"))),
197 max_value=min(self.max_value, next_down(float("inf"))),
198 allow_nan=False,
199 smallest_nonzero_magnitude=self.smallest_nonzero_magnitude,
200 )
201 if condition is math.isinf:
202 if permitted_infs := [
203 x
204 for x in (-math.inf, math.inf)
205 if self.min_value <= x <= self.max_value
206 ]:
207 return SampledFromStrategy(permitted_infs)
208 return nothing()
209 if condition is math.isnan:
210 if not self.allow_nan:
211 return nothing()
212 return NanStrategy()
213
214 constraints, pred = get_float_predicate_bounds(condition)
215 if not constraints:
216 return super().filter(pred)
217 min_bound = max(constraints.get("min_value", -math.inf), self.min_value)
218 max_bound = min(constraints.get("max_value", math.inf), self.max_value)
219
220 # Adjustments for allow_subnormal=False, if any need to be made
221 if -self.smallest_nonzero_magnitude < min_bound < 0:
222 min_bound = -0.0
223 elif 0 < min_bound < self.smallest_nonzero_magnitude:
224 min_bound = self.smallest_nonzero_magnitude
225 if -self.smallest_nonzero_magnitude < max_bound < 0:
226 max_bound = -self.smallest_nonzero_magnitude
227 elif 0 < max_bound < self.smallest_nonzero_magnitude:
228 max_bound = 0.0
229
230 if min_bound > max_bound:
231 return nothing()
232 if (
233 min_bound > self.min_value
234 or self.max_value > max_bound
235 or (self.allow_nan and (-math.inf < min_bound or max_bound < math.inf))
236 ):
237 self = type(self)(
238 min_value=min_bound,
239 max_value=max_bound,
240 allow_nan=False,
241 smallest_nonzero_magnitude=self.smallest_nonzero_magnitude,
242 )
243 if pred is None:
244 return self
245 return super().filter(pred)
246
247
248@cacheable
249@defines_strategy(force_reusable_values=True)
250def floats(
251 min_value: Optional[Real] = None,
252 max_value: Optional[Real] = None,
253 *,
254 allow_nan: Optional[bool] = None,
255 allow_infinity: Optional[bool] = None,
256 allow_subnormal: Optional[bool] = None,
257 width: Literal[16, 32, 64] = 64,
258 exclude_min: bool = False,
259 exclude_max: bool = False,
260) -> SearchStrategy[float]:
261 """Returns a strategy which generates floats.
262
263 - If min_value is not None, all values will be ``>= min_value``
264 (or ``> min_value`` if ``exclude_min``).
265 - If max_value is not None, all values will be ``<= max_value``
266 (or ``< max_value`` if ``exclude_max``).
267 - If min_value or max_value is not None, it is an error to enable
268 allow_nan.
269 - If both min_value and max_value are not None, it is an error to enable
270 allow_infinity.
271 - If inferred values range does not include subnormal values, it is an error
272 to enable allow_subnormal.
273
274 Where not explicitly ruled out by the bounds,
275 :wikipedia:`subnormals <Subnormal_number>`, infinities, and NaNs are possible
276 values generated by this strategy.
277
278 The width argument specifies the maximum number of bits of precision
279 required to represent the generated float. Valid values are 16, 32, or 64.
280 Passing ``width=32`` will still use the builtin 64-bit :class:`~python:float` class,
281 but always for values which can be exactly represented as a 32-bit float.
282
283 The exclude_min and exclude_max argument can be used to generate numbers
284 from open or half-open intervals, by excluding the respective endpoints.
285 Excluding either signed zero will also exclude the other.
286 Attempting to exclude an endpoint which is None will raise an error;
287 use ``allow_infinity=False`` to generate finite floats. You can however
288 use e.g. ``min_value=-math.inf, exclude_min=True`` to exclude only
289 one infinite endpoint.
290
291 Examples from this strategy have a complicated and hard to explain
292 shrinking behaviour, but it tries to improve "human readability". Finite
293 numbers will be preferred to infinity and infinity will be preferred to
294 NaN.
295 """
296 check_type(bool, exclude_min, "exclude_min")
297 check_type(bool, exclude_max, "exclude_max")
298
299 if allow_nan is None:
300 allow_nan = bool(min_value is None and max_value is None)
301 elif allow_nan and (min_value is not None or max_value is not None):
302 raise InvalidArgument(f"Cannot have {allow_nan=}, with min_value or max_value")
303
304 if width not in (16, 32, 64):
305 raise InvalidArgument(
306 f"Got {width=}, but the only valid values "
307 "are the integers 16, 32, and 64."
308 )
309
310 check_valid_bound(min_value, "min_value")
311 check_valid_bound(max_value, "max_value")
312
313 if math.copysign(1.0, -0.0) == 1.0: # pragma: no cover
314 raise FloatingPointError(
315 "Your Python install can't represent -0.0, which is required by the "
316 "IEEE-754 floating-point specification. This is probably because it was "
317 "compiled with an unsafe option like -ffast-math; for a more detailed "
318 "explanation see https://simonbyrne.github.io/notes/fastmath/"
319 )
320 if allow_subnormal and next_up(0.0, width=width) == 0: # pragma: no cover
321 # Not worth having separate CI envs and dependencies just to cover this branch;
322 # discussion in https://github.com/HypothesisWorks/hypothesis/issues/3092
323 #
324 # Erroring out here ensures that the database contents are interpreted
325 # consistently - which matters for such a foundational strategy, even if it's
326 # not always true for all user-composed strategies further up the stack.
327 from _hypothesis_ftz_detector import identify_ftz_culprits
328
329 try:
330 ftz_pkg = identify_ftz_culprits()
331 except Exception:
332 ftz_pkg = None
333 if ftz_pkg:
334 ftz_msg = (
335 f"This seems to be because the `{ftz_pkg}` package was compiled with "
336 f"-ffast-math or a similar option, which sets global processor state "
337 f"- see https://simonbyrne.github.io/notes/fastmath/ for details. "
338 f"If you don't know why {ftz_pkg} is installed, `pipdeptree -rp "
339 f"{ftz_pkg}` will show which packages depend on it."
340 )
341 else:
342 ftz_msg = (
343 "This is usually because something was compiled with -ffast-math "
344 "or a similar option, which sets global processor state. See "
345 "https://simonbyrne.github.io/notes/fastmath/ for a more detailed "
346 "writeup - and good luck!"
347 )
348 raise FloatingPointError(
349 f"Got {allow_subnormal=}, but we can't represent "
350 f"subnormal floats right now, in violation of the IEEE-754 floating-point "
351 f"specification. {ftz_msg}"
352 )
353
354 min_arg, max_arg = min_value, max_value
355 if min_value is not None:
356 min_value = float_of(min_value, width)
357 assert isinstance(min_value, float)
358 if max_value is not None:
359 max_value = float_of(max_value, width)
360 assert isinstance(max_value, float)
361
362 if min_value != min_arg:
363 raise InvalidArgument(
364 f"min_value={min_arg!r} cannot be exactly represented as a float "
365 f"of width {width} - use {min_value=} instead."
366 )
367 if max_value != max_arg:
368 raise InvalidArgument(
369 f"max_value={max_arg!r} cannot be exactly represented as a float "
370 f"of width {width} - use {max_value=} instead."
371 )
372
373 if exclude_min and (min_value is None or min_value == math.inf):
374 raise InvalidArgument(f"Cannot exclude {min_value=}")
375 if exclude_max and (max_value is None or max_value == -math.inf):
376 raise InvalidArgument(f"Cannot exclude {max_value=}")
377
378 assumed_allow_subnormal = allow_subnormal is None or allow_subnormal
379 if min_value is not None and (
380 exclude_min or (min_arg is not None and min_value < min_arg)
381 ):
382 min_value = next_up_normal(
383 min_value, width, allow_subnormal=assumed_allow_subnormal
384 )
385 if min_value == min_arg:
386 assert min_value == min_arg == 0
387 assert is_negative(min_arg)
388 assert not is_negative(min_value)
389 min_value = next_up_normal(
390 min_value, width, allow_subnormal=assumed_allow_subnormal
391 )
392 assert min_value > min_arg # type: ignore
393 if max_value is not None and (
394 exclude_max or (max_arg is not None and max_value > max_arg)
395 ):
396 max_value = next_down_normal(
397 max_value, width, allow_subnormal=assumed_allow_subnormal
398 )
399 if max_value == max_arg:
400 assert max_value == max_arg == 0
401 assert is_negative(max_value)
402 assert not is_negative(max_arg)
403 max_value = next_down_normal(
404 max_value, width, allow_subnormal=assumed_allow_subnormal
405 )
406 assert max_value < max_arg # type: ignore
407
408 if min_value == -math.inf:
409 min_value = None
410 if max_value == math.inf:
411 max_value = None
412
413 bad_zero_bounds = (
414 min_value == max_value == 0
415 and is_negative(max_value)
416 and not is_negative(min_value)
417 )
418 if (
419 min_value is not None
420 and max_value is not None
421 and (min_value > max_value or bad_zero_bounds)
422 ):
423 # This is a custom alternative to check_valid_interval, because we want
424 # to include the bit-width and exclusion information in the message.
425 msg = (
426 f"There are no {width}-bit floating-point values between "
427 f"min_value={min_arg!r} and max_value={max_arg!r}"
428 )
429 if exclude_min or exclude_max:
430 msg += f", {exclude_min=} and {exclude_max=}"
431 raise InvalidArgument(msg)
432
433 if allow_infinity is None:
434 allow_infinity = bool(min_value is None or max_value is None)
435 elif allow_infinity:
436 if min_value is not None and max_value is not None:
437 raise InvalidArgument(
438 f"Cannot have {allow_infinity=}, with both min_value and max_value"
439 )
440 elif min_value == math.inf:
441 if min_arg == math.inf:
442 raise InvalidArgument("allow_infinity=False excludes min_value=inf")
443 raise InvalidArgument(
444 f"exclude_min=True turns min_value={min_arg!r} into inf, "
445 "but allow_infinity=False"
446 )
447 elif max_value == -math.inf:
448 if max_arg == -math.inf:
449 raise InvalidArgument("allow_infinity=False excludes max_value=-inf")
450 raise InvalidArgument(
451 f"exclude_max=True turns max_value={max_arg!r} into -inf, "
452 "but allow_infinity=False"
453 )
454
455 smallest_normal = width_smallest_normals[width]
456 if allow_subnormal is None:
457 if min_value is not None and max_value is not None:
458 if min_value == max_value:
459 allow_subnormal = -smallest_normal < min_value < smallest_normal
460 else:
461 allow_subnormal = (
462 min_value < smallest_normal and max_value > -smallest_normal
463 )
464 elif min_value is not None:
465 allow_subnormal = min_value < smallest_normal
466 elif max_value is not None:
467 allow_subnormal = max_value > -smallest_normal
468 else:
469 allow_subnormal = True
470 if allow_subnormal:
471 if min_value is not None and min_value >= smallest_normal:
472 raise InvalidArgument(
473 f"allow_subnormal=True, but minimum value {min_value} "
474 f"excludes values below float{width}'s "
475 f"smallest positive normal {smallest_normal}"
476 )
477 if max_value is not None and max_value <= -smallest_normal:
478 raise InvalidArgument(
479 f"allow_subnormal=True, but maximum value {max_value} "
480 f"excludes values above float{width}'s "
481 f"smallest negative normal {-smallest_normal}"
482 )
483
484 if min_value is None:
485 min_value = float("-inf")
486 if max_value is None:
487 max_value = float("inf")
488 if not allow_infinity:
489 min_value = max(min_value, next_up(float("-inf")))
490 max_value = min(max_value, next_down(float("inf")))
491 assert isinstance(min_value, float)
492 assert isinstance(max_value, float)
493 smallest_nonzero_magnitude = (
494 SMALLEST_SUBNORMAL if allow_subnormal else smallest_normal
495 )
496 result: SearchStrategy = FloatStrategy(
497 min_value=min_value,
498 max_value=max_value,
499 allow_nan=allow_nan,
500 smallest_nonzero_magnitude=smallest_nonzero_magnitude,
501 )
502
503 if width < 64:
504
505 def downcast(x: float) -> float:
506 try:
507 return float_of(x, width)
508 except OverflowError: # pragma: no cover
509 reject()
510
511 result = result.map(downcast)
512 return result
513
514
515class NanStrategy(SearchStrategy[float]):
516 """Strategy for sampling the space of nan float values."""
517
518 def do_draw(self, data: ConjectureData) -> float:
519 # Nans must have all exponent bits and the first mantissa bit set, so
520 # we generate by taking 64 random bits and setting the required ones.
521 sign_bit = int(data.draw_boolean()) << 63
522 nan_bits = float_to_int(math.nan)
523 mantissa_bits = data.draw_integer(0, 2**52 - 1)
524 return int_to_float(sign_bit | nan_bits | mantissa_bits)