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