Skip to content

Commit 40daade

Browse files
authored
Merge pull request #3923 from tybug/more-shrinker-ir
Migrate `reorder_examples` to the IR
2 parents 2129503 + b40999d commit 40daade

File tree

11 files changed

+254
-71
lines changed

11 files changed

+254
-71
lines changed

hypothesis-python/RELEASE.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
RELEASE_TYPE: patch
2+
3+
This patch continues our work on refactoring shrinker internals (:issue:`3921`).

hypothesis-python/src/hypothesis/internal/conjecture/data.py

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,14 @@ def end(self) -> int:
289289
"""
290290
return self.owner.ends[self.index]
291291

292+
@property
293+
def ir_start(self) -> int:
294+
return self.owner.ir_starts[self.index]
295+
296+
@property
297+
def ir_end(self) -> int:
298+
return self.owner.ir_ends[self.index]
299+
292300
@property
293301
def depth(self):
294302
"""Depth of this example in the example tree. The top-level example has a
@@ -529,6 +537,32 @@ def starts(self) -> IntList:
529537
def ends(self) -> IntList:
530538
return self.starts_and_ends[1]
531539

540+
class _ir_starts_and_ends(ExampleProperty):
541+
def begin(self):
542+
self.starts = IntList.of_length(len(self.examples))
543+
self.ends = IntList.of_length(len(self.examples))
544+
545+
def start_example(self, i: int, label_index: int) -> None:
546+
self.starts[i] = self.ir_node_count
547+
548+
def stop_example(self, i: int, *, discarded: bool) -> None:
549+
self.ends[i] = self.ir_node_count
550+
551+
def finish(self) -> Tuple[IntList, IntList]:
552+
return (self.starts, self.ends)
553+
554+
ir_starts_and_ends: "Tuple[IntList, IntList]" = calculated_example_property(
555+
_ir_starts_and_ends
556+
)
557+
558+
@property
559+
def ir_starts(self) -> IntList:
560+
return self.ir_starts_and_ends[0]
561+
562+
@property
563+
def ir_ends(self) -> IntList:
564+
return self.ir_starts_and_ends[1]
565+
532566
class _discarded(ExampleProperty):
533567
def begin(self) -> None:
534568
self.result: "Set[int]" = set() # type: ignore # IntList in parent class
@@ -910,7 +944,7 @@ def draw_boolean(
910944
pass
911945

912946

913-
@attr.s(slots=True)
947+
@attr.s(slots=True, repr=False, eq=False)
914948
class IRNode:
915949
ir_type: IRTypeName = attr.ib()
916950
value: IRType = attr.ib()
@@ -928,6 +962,22 @@ def copy(self, *, with_value: IRType) -> "IRNode":
928962
was_forced=self.was_forced,
929963
)
930964

965+
def __eq__(self, other):
966+
if not isinstance(other, IRNode):
967+
return NotImplemented
968+
969+
return (
970+
self.ir_type == other.ir_type
971+
and ir_value_equal(self.ir_type, self.value, other.value)
972+
and ir_kwargs_equal(self.ir_type, self.kwargs, other.kwargs)
973+
and self.was_forced == other.was_forced
974+
)
975+
976+
def __repr__(self):
977+
# repr to avoid "BytesWarning: str() on a bytes instance" for bytes nodes
978+
forced_marker = " [forced]" if self.was_forced else ""
979+
return f"{self.ir_type} {self.value!r}{forced_marker} {self.kwargs!r}"
980+
931981

932982
def ir_value_permitted(value, ir_type, kwargs):
933983
if ir_type == "integer":
@@ -962,6 +1012,24 @@ def ir_value_permitted(value, ir_type, kwargs):
9621012
raise NotImplementedError(f"unhandled type {type(value)} of ir value {value}")
9631013

9641014

1015+
def ir_value_equal(ir_type, v1, v2):
1016+
if ir_type != "float":
1017+
return v1 == v2
1018+
return float_to_int(v1) == float_to_int(v2)
1019+
1020+
1021+
def ir_kwargs_equal(ir_type, kwargs1, kwargs2):
1022+
if ir_type != "float":
1023+
return kwargs1 == kwargs2
1024+
return (
1025+
float_to_int(kwargs1["min_value"]) == float_to_int(kwargs2["min_value"])
1026+
and float_to_int(kwargs1["max_value"]) == float_to_int(kwargs2["max_value"])
1027+
and kwargs1["allow_nan"] == kwargs2["allow_nan"]
1028+
and kwargs1["smallest_nonzero_magnitude"]
1029+
== kwargs2["smallest_nonzero_magnitude"]
1030+
)
1031+
1032+
9651033
@dataclass_transform()
9661034
@attr.s(slots=True)
9671035
class ConjectureResult:
@@ -1876,9 +1944,10 @@ def draw_integer(
18761944

18771945
if self.ir_tree_nodes is not None and observe:
18781946
node = self._pop_ir_tree_node("integer", kwargs)
1879-
assert isinstance(node.value, int)
1880-
forced = node.value
1881-
fake_forced = not node.was_forced
1947+
if forced is None:
1948+
assert isinstance(node.value, int)
1949+
forced = node.value
1950+
fake_forced = True
18821951

18831952
value = self.provider.draw_integer(
18841953
**kwargs, forced=forced, fake_forced=fake_forced
@@ -1932,9 +2001,10 @@ def draw_float(
19322001

19332002
if self.ir_tree_nodes is not None and observe:
19342003
node = self._pop_ir_tree_node("float", kwargs)
1935-
assert isinstance(node.value, float)
1936-
forced = node.value
1937-
fake_forced = not node.was_forced
2004+
if forced is None:
2005+
assert isinstance(node.value, float)
2006+
forced = node.value
2007+
fake_forced = True
19382008

19392009
value = self.provider.draw_float(
19402010
**kwargs, forced=forced, fake_forced=fake_forced
@@ -1973,9 +2043,10 @@ def draw_string(
19732043
)
19742044
if self.ir_tree_nodes is not None and observe:
19752045
node = self._pop_ir_tree_node("string", kwargs)
1976-
assert isinstance(node.value, str)
1977-
forced = node.value
1978-
fake_forced = not node.was_forced
2046+
if forced is None:
2047+
assert isinstance(node.value, str)
2048+
forced = node.value
2049+
fake_forced = True
19792050

19802051
value = self.provider.draw_string(
19812052
**kwargs, forced=forced, fake_forced=fake_forced
@@ -2008,9 +2079,10 @@ def draw_bytes(
20082079

20092080
if self.ir_tree_nodes is not None and observe:
20102081
node = self._pop_ir_tree_node("bytes", kwargs)
2011-
assert isinstance(node.value, bytes)
2012-
forced = node.value
2013-
fake_forced = not node.was_forced
2082+
if forced is None:
2083+
assert isinstance(node.value, bytes)
2084+
forced = node.value
2085+
fake_forced = True
20142086

20152087
value = self.provider.draw_bytes(
20162088
**kwargs, forced=forced, fake_forced=fake_forced
@@ -2049,9 +2121,10 @@ def draw_boolean(
20492121

20502122
if self.ir_tree_nodes is not None and observe:
20512123
node = self._pop_ir_tree_node("boolean", kwargs)
2052-
assert isinstance(node.value, bool)
2053-
forced = node.value
2054-
fake_forced = not node.was_forced
2124+
if forced is None:
2125+
assert isinstance(node.value, bool)
2126+
forced = node.value
2127+
fake_forced = True
20552128

20562129
value = self.provider.draw_boolean(
20572130
**kwargs, forced=forced, fake_forced=fake_forced
@@ -2113,7 +2186,7 @@ def _pop_ir_tree_node(self, ir_type: IRTypeName, kwargs: IRKWargsType) -> IRNode
21132186
# that is allowed by the expected kwargs, then we can coerce this node
21142187
# into an aligned one by using its value. It's unclear how useful this is.
21152188
if not ir_value_permitted(node.value, node.ir_type, kwargs):
2116-
self.mark_invalid() # pragma: no cover # FIXME @tybug
2189+
self.mark_invalid()
21172190

21182191
return node
21192192

hypothesis-python/src/hypothesis/internal/conjecture/engine.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,11 @@ def __stoppable_test_function(self, data):
239239
# correct engine.
240240
raise
241241

242+
def ir_tree_to_data(self, ir_tree_nodes):
243+
data = ConjectureData.for_ir_tree(ir_tree_nodes)
244+
self.__stoppable_test_function(data)
245+
return data
246+
242247
def test_function(self, data):
243248
if self.__pending_call_explanation is not None:
244249
self.debug(self.__pending_call_explanation)
@@ -316,8 +321,7 @@ def test_function(self, data):
316321

317322
# drive the ir tree through the test function to convert it
318323
# to a buffer
319-
data = ConjectureData.for_ir_tree(data.examples.ir_tree_nodes)
320-
self.__stoppable_test_function(data)
324+
data = self.ir_tree_to_data(data.examples.ir_tree_nodes)
321325
self.__data_cache[data.buffer] = data.as_result()
322326

323327
key = data.interesting_origin

hypothesis-python/src/hypothesis/internal/conjecture/junkdrawer.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535

3636
ARRAY_CODES = ["B", "H", "I", "L", "Q", "O"]
3737

38+
T = TypeVar("T")
39+
3840

3941
def array_or_list(
4042
code: str, contents: Iterable[int]
@@ -45,25 +47,25 @@ def array_or_list(
4547

4648

4749
def replace_all(
48-
buffer: Sequence[int],
49-
replacements: Iterable[Tuple[int, int, Sequence[int]]],
50-
) -> bytes:
51-
"""Substitute multiple replacement values into a buffer.
50+
ls: Sequence[T],
51+
replacements: Iterable[Tuple[int, int, Sequence[T]]],
52+
) -> List[T]:
53+
"""Substitute multiple replacement values into a list.
5254
5355
Replacements is a list of (start, end, value) triples.
5456
"""
5557

56-
result = bytearray()
58+
result: List[T] = []
5759
prev = 0
5860
offset = 0
5961
for u, v, r in replacements:
60-
result.extend(buffer[prev:u])
62+
result.extend(ls[prev:u])
6163
result.extend(r)
6264
prev = v
6365
offset += len(r) - (v - u)
64-
result.extend(buffer[prev:])
65-
assert len(result) == len(buffer) + offset
66-
return bytes(result)
66+
result.extend(ls[prev:])
67+
assert len(result) == len(ls) + offset
68+
return result
6769

6870

6971
NEXT_ARRAY_CODE = dict(zip(ARRAY_CODES, ARRAY_CODES[1:]))
@@ -190,9 +192,6 @@ def uniform(random: Random, n: int) -> bytes:
190192
return random.getrandbits(n * 8).to_bytes(n, "big")
191193

192194

193-
T = TypeVar("T")
194-
195-
196195
class LazySequenceCopy:
197196
"""A "copy" of a sequence that works by inserting a mask in front
198197
of the underlying sequence, so that you can mutate it without changing

hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -376,8 +376,7 @@ def calls(self):
376376
return self.engine.call_count
377377

378378
def consider_new_tree(self, tree):
379-
data = ConjectureData.for_ir_tree(tree)
380-
self.engine.test_function(data)
379+
data = self.engine.ir_tree_to_data(tree)
381380

382381
return self.consider_new_buffer(data.buffer)
383382

@@ -1413,20 +1412,31 @@ def test_not_equal(x, y):
14131412
ex = chooser.choose(self.examples)
14141413
label = chooser.choose(ex.children).label
14151414

1416-
group = [c for c in ex.children if c.label == label]
1417-
if len(group) <= 1:
1415+
examples = [c for c in ex.children if c.label == label]
1416+
if len(examples) <= 1:
14181417
return
1419-
14201418
st = self.shrink_target
1421-
pieces = [st.buffer[ex.start : ex.end] for ex in group]
1422-
endpoints = [(ex.start, ex.end) for ex in group]
1419+
endpoints = [(ex.ir_start, ex.ir_end) for ex in examples]
14231420

14241421
Ordering.shrink(
1425-
pieces,
1426-
lambda ls: self.consider_new_buffer(
1427-
replace_all(st.buffer, [(u, v, r) for (u, v), r in zip(endpoints, ls)])
1422+
range(len(examples)),
1423+
lambda indices: self.consider_new_tree(
1424+
replace_all(
1425+
st.examples.ir_nodes,
1426+
[
1427+
(
1428+
u,
1429+
v,
1430+
st.examples.ir_nodes[
1431+
examples[i].ir_start : examples[i].ir_end
1432+
],
1433+
)
1434+
for (u, v), i in zip(endpoints, indices)
1435+
],
1436+
)
14281437
),
14291438
random=self.random,
1439+
key=lambda i: st.buffer[examples[i].start : examples[i].end],
14301440
)
14311441

14321442
def run_block_program(self, i, description, original, repeats=1):

hypothesis-python/tests/conjecture/common.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,18 @@ def draw_boolean_kwargs(draw, *, use_forced=False):
277277
return {"p": p, "forced": forced}
278278

279279

280+
def kwargs_strategy(ir_type):
281+
return {
282+
"boolean": draw_boolean_kwargs(),
283+
"integer": draw_integer_kwargs(),
284+
"float": draw_float_kwargs(),
285+
"bytes": draw_bytes_kwargs(),
286+
"string": draw_string_kwargs(),
287+
}[ir_type]
288+
289+
280290
def ir_types_and_kwargs():
281-
options = [
282-
("boolean", draw_boolean_kwargs()),
283-
("integer", draw_integer_kwargs()),
284-
("float", draw_float_kwargs()),
285-
("bytes", draw_bytes_kwargs()),
286-
("string", draw_string_kwargs()),
287-
]
288-
return st.one_of(st.tuples(st.just(name), kws) for name, kws in options)
291+
options = ["boolean", "integer", "float", "bytes", "string"]
292+
return st.one_of(
293+
st.tuples(st.just(name), kwargs_strategy(name)) for name in options
294+
)

hypothesis-python/tests/conjecture/test_dfa.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,16 @@
1414

1515
import pytest
1616

17-
from hypothesis import assume, example, given, note, reject, settings, strategies as st
17+
from hypothesis import (
18+
HealthCheck,
19+
assume,
20+
example,
21+
given,
22+
note,
23+
reject,
24+
settings,
25+
strategies as st,
26+
)
1827
from hypothesis.internal.conjecture.dfa import DEAD, ConcreteDFA
1928

2029

@@ -112,7 +121,8 @@ def test_canonicalised_matches_same_strings(dfa, via_repr):
112121
)
113122

114123

115-
@settings(max_examples=20)
124+
# filters about 80% of examples. should potentially improve at some point.
125+
@settings(max_examples=20, suppress_health_check=[HealthCheck.filter_too_much])
116126
@given(dfas())
117127
def test_has_string_of_max_length(dfa):
118128
length = dfa.max_length(dfa.start)

0 commit comments

Comments
 (0)