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