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 collections.abc import Callable
13from sys import float_info
14from typing import overload
15
16from hypothesis._native.internal.floats import (
17 count_between_floats as count_between_floats,
18 float_of as float_of,
19 float_to_int as float_to_int,
20 int_to_float as int_to_float,
21 is_negative as is_negative,
22 next_down as next_down,
23 next_down_normal as next_down_normal,
24 next_up as next_up,
25 next_up_normal as next_up_normal,
26 width_smallest_normals as width_smallest_normals,
27)
28
29assert width_smallest_normals(64) == float_info.min
30
31mantissa_mask = (1 << 52) - 1
32
33
34def make_float_clamper(
35 min_value: float,
36 max_value: float,
37 *,
38 allow_nan: bool,
39 smallest_nonzero_magnitude: float,
40) -> Callable[[float], float]:
41 """
42 Return a function that clamps positive floats into the given bounds.
43 """
44 from hypothesis.internal.conjecture.choice import choice_permitted
45
46 assert sign_aware_lte(min_value, max_value)
47 range_size = min(max_value - min_value, float_info.max)
48
49 def float_clamper(f: float) -> float:
50 if choice_permitted(
51 f,
52 {
53 "min_value": min_value,
54 "max_value": max_value,
55 "allow_nan": allow_nan,
56 "smallest_nonzero_magnitude": smallest_nonzero_magnitude,
57 },
58 ):
59 return f
60 # Outside bounds; pick a new value, sampled from the allowed range,
61 # using the mantissa bits.
62 mant = float_to_int(abs(f)) & mantissa_mask
63 f = min_value + range_size * (mant / mantissa_mask)
64
65 # if we resampled into the space disallowed by smallest_nonzero_magnitude,
66 # default to smallest_nonzero_magnitude.
67 if 0 < abs(f) < smallest_nonzero_magnitude:
68 f = smallest_nonzero_magnitude
69 # we must have either -smallest_nonzero_magnitude <= min_value or
70 # smallest_nonzero_magnitude >= max_value, or no values would be
71 # possible. If smallest_nonzero_magnitude is not valid (because it's
72 # larger than max_value), then -smallest_nonzero_magnitude must be valid.
73 if smallest_nonzero_magnitude > max_value:
74 f *= -1
75
76 # Re-enforce the bounds (just in case of floating point arithmetic error)
77 return clamp(min_value, f, max_value)
78
79 return float_clamper
80
81
82def sign_aware_lte(x: float | int, y: float | int) -> bool:
83 """Less-than-or-equals, but strictly orders -0.0 and 0.0"""
84 if x == 0.0 == y:
85 return math.copysign(1.0, x) <= math.copysign(1.0, y)
86 else:
87 return x <= y
88
89
90@overload
91def clamp(lower: int, value: int, upper: int) -> int: ...
92@overload
93def clamp(lower: float, value: float, upper: float) -> float: ...
94def clamp(lower: float | int, value: float | int, upper: float | int) -> float | int:
95 """Given a value and lower/upper bounds, 'clamp' the value so that
96 it satisfies lower <= value <= upper. NaN is mapped to lower."""
97 # this seems pointless (and is for integers), but handles the -0.0/0.0 case.
98 if not sign_aware_lte(lower, value):
99 return lower
100 if not sign_aware_lte(value, upper):
101 return upper
102 return value
103
104
105SMALLEST_SUBNORMAL = next_up(0.0)
106SIGNALING_NAN = int_to_float(0x7FF8_0000_0000_0001) # nonzero mantissa
107MAX_PRECISE_INTEGER = 2**53
108assert math.isnan(SIGNALING_NAN)
109assert math.copysign(1, SIGNALING_NAN) == 1