Skip to content

Commit

Permalink
migrate float shrinker to the ir
Browse files Browse the repository at this point in the history
  • Loading branch information
tybug committed Mar 10, 2024
1 parent 6662180 commit b6bb8b4
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 33 deletions.
26 changes: 24 additions & 2 deletions hypothesis-python/src/hypothesis/internal/conjecture/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,17 @@ class IRNode:
kwargs: IRKWargsType = attr.ib()
was_forced: bool = attr.ib()

def copy(self, *, with_value: IRType) -> "IRNode":
# we may want to allow this combination in the future, but for now it's
# a footgun.
assert not self.was_forced, "modifying a forced node doesn't make sense"
return IRNode(
ir_type=self.ir_type,
value=with_value,
kwargs=self.kwargs,
was_forced=self.was_forced,
)


@dataclass_transform()
@attr.s(slots=True)
Expand Down Expand Up @@ -2049,9 +2060,20 @@ def _pooled_kwargs(self, ir_type, kwargs):

def _pop_ir_tree_node(self, ir_type: IRTypeName, kwargs: IRKWargsType) -> IRNode:
assert self.ir_tree_nodes is not None

if self.ir_tree_nodes == []:
self.mark_overrun()

node = self.ir_tree_nodes.pop(0)
assert node.ir_type == ir_type
assert kwargs == node.kwargs
if node.ir_type != ir_type or node.kwargs != kwargs:
# Unlike buffers, not every ir tree is a valid choice sequence. If
# we want to maintain determinism we don't have many options here
# beyond giving up when a modified tree becomes misaligned.
#
# For what it's worth, misaligned buffers — albeit valid — are rather
# unlikely to be *useful* buffers, so this isn't an enormous
# downgrade.
self.mark_overrun()

return node

Expand Down
67 changes: 37 additions & 30 deletions hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# 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
from collections import defaultdict
from typing import TYPE_CHECKING, Callable, Dict, Optional

Expand All @@ -19,14 +20,9 @@
prefix_selection_order,
random_selection_order,
)
from hypothesis.internal.conjecture.data import (
DRAW_FLOAT_LABEL,
ConjectureData,
ConjectureResult,
Status,
)
from hypothesis.internal.conjecture.data import 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.floats import is_simple
from hypothesis.internal.conjecture.junkdrawer import (
binary_search,
find_integer,
Expand Down Expand Up @@ -379,6 +375,12 @@ def calls(self):
test function."""
return self.engine.call_count

def consider_new_tree(self, tree):
data = ConjectureData.for_ir_tree(tree)
self.engine.test_function(data)

return self.consider_new_buffer(data.buffer)

def consider_new_buffer(self, buffer):
"""Returns True if after running this buffer the result would be
the current shrink_target."""
Expand Down Expand Up @@ -774,6 +776,10 @@ def buffer(self):
def blocks(self):
return self.shrink_target.blocks

@property
def nodes(self):
return self.shrink_target.examples.ir_tree_nodes

@property
def examples(self):
return self.shrink_target.examples
Expand Down Expand Up @@ -1207,31 +1213,32 @@ def minimize_floats(self, chooser):
anything particularly meaningful for non-float values.
"""

ex = chooser.choose(
self.examples,
lambda ex: (
ex.label == DRAW_FLOAT_LABEL
and len(ex.children) == 2
and ex.children[1].length == 8
),
node = chooser.choose(
self.nodes, lambda node: node.ir_type == "float" and not node.was_forced
)
# avoid shrinking integer-valued floats. In our current ordering, these
# are already simpler than all other floats, so it's better to shrink
# them in other passes.
if is_simple(node.value):
return

u = ex.children[1].start
v = ex.children[1].end
buf = self.shrink_target.buffer
b = buf[u:v]
f = lex_to_float(int_from_bytes(b))
b2 = int_to_bytes(float_to_lex(f), 8)
if b == b2 or self.consider_new_buffer(buf[:u] + b2 + buf[v:]):
Float.shrink(
f,
lambda x: self.consider_new_buffer(
self.shrink_target.buffer[:u]
+ int_to_bytes(float_to_lex(x), 8)
+ self.shrink_target.buffer[v:]
),
random=self.random,
)
i = self.nodes.index(node)
# the Float shrinker was only built to handle positive floats. We'll
# shrink the positive portion and reapply the sign after, which is
# equivalent to this shrinker's previous behavior. We'll want to refactor
# Float to handle negative floats natively in the future. (likely a pure
# code quality change, with no shrinking impact.)
sign = math.copysign(1.0, node.value)
Float.shrink(
abs(node.value),
lambda val: self.consider_new_tree(
self.nodes[:i]
+ [node.copy(with_value=sign * val)]
+ self.nodes[i + 1 :]
),
random=self.random,
node=node,
)

@defines_shrink_pass()
def redistribute_block_pairs(self, chooser):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,28 @@
from hypothesis.internal.conjecture.floats import float_to_lex
from hypothesis.internal.conjecture.shrinking.common import Shrinker
from hypothesis.internal.conjecture.shrinking.integer import Integer
from hypothesis.internal.floats import sign_aware_lte

MAX_PRECISE_INTEGER = 2**53


class Float(Shrinker):
def setup(self):
def setup(self, node):
self.NAN = math.nan
self.debugging_enabled = True
self.node = node

def consider(self, value):
min_value = self.node.kwargs["min_value"]
max_value = self.node.kwargs["max_value"]
if not math.isnan(value) and not (
sign_aware_lte(min_value, value) and sign_aware_lte(value, max_value)
):
self.debug(
f"rejecting {value} as out of bounds for [{min_value}, {max_value}]"
)
return False
return super().consider(value)

def make_immutable(self, f):
f = float(f)
Expand Down

0 comments on commit b6bb8b4

Please sign in to comment.