crosshair-tool 0.0.56__cp39-cp39-macosx_11_0_arm64.whl → 0.0.100__cp39-cp39-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- _crosshair_tracers.cpython-39-darwin.so +0 -0
- crosshair/__init__.py +1 -1
- crosshair/_mark_stacks.h +51 -24
- crosshair/_tracers.h +9 -5
- crosshair/_tracers_test.py +19 -9
- crosshair/auditwall.py +9 -8
- crosshair/auditwall_test.py +31 -19
- crosshair/codeconfig.py +3 -2
- crosshair/condition_parser.py +17 -133
- crosshair/condition_parser_test.py +54 -96
- crosshair/conftest.py +1 -1
- crosshair/copyext.py +91 -22
- crosshair/copyext_test.py +33 -0
- crosshair/core.py +259 -203
- crosshair/core_and_libs.py +20 -0
- crosshair/core_regestered_types_test.py +82 -0
- crosshair/core_test.py +693 -664
- crosshair/diff_behavior.py +76 -21
- crosshair/diff_behavior_test.py +132 -23
- crosshair/dynamic_typing.py +128 -18
- crosshair/dynamic_typing_test.py +91 -4
- crosshair/enforce.py +1 -6
- crosshair/enforce_test.py +15 -23
- crosshair/examples/check_examples_test.py +2 -1
- crosshair/fnutil.py +2 -3
- crosshair/fnutil_test.py +0 -7
- crosshair/fuzz_core_test.py +70 -83
- crosshair/libimpl/arraylib.py +10 -7
- crosshair/libimpl/binascii_ch_test.py +30 -0
- crosshair/libimpl/binascii_test.py +67 -0
- crosshair/libimpl/binasciilib.py +150 -0
- crosshair/libimpl/bisectlib_test.py +5 -5
- crosshair/libimpl/builtinslib.py +1002 -682
- crosshair/libimpl/builtinslib_ch_test.py +108 -30
- crosshair/libimpl/builtinslib_test.py +431 -143
- crosshair/libimpl/codecslib.py +22 -2
- crosshair/libimpl/codecslib_test.py +41 -9
- crosshair/libimpl/collectionslib.py +44 -8
- crosshair/libimpl/collectionslib_test.py +108 -20
- crosshair/libimpl/copylib.py +1 -1
- crosshair/libimpl/copylib_test.py +18 -0
- crosshair/libimpl/datetimelib.py +84 -67
- crosshair/libimpl/datetimelib_ch_test.py +12 -7
- crosshair/libimpl/datetimelib_test.py +5 -6
- crosshair/libimpl/decimallib.py +5257 -0
- crosshair/libimpl/decimallib_ch_test.py +78 -0
- crosshair/libimpl/decimallib_test.py +76 -0
- crosshair/libimpl/encodings/_encutil.py +21 -11
- crosshair/libimpl/fractionlib.py +16 -0
- crosshair/libimpl/fractionlib_test.py +80 -0
- crosshair/libimpl/functoolslib.py +19 -7
- crosshair/libimpl/functoolslib_test.py +22 -6
- crosshair/libimpl/hashliblib.py +30 -0
- crosshair/libimpl/hashliblib_test.py +18 -0
- crosshair/libimpl/heapqlib.py +32 -5
- crosshair/libimpl/heapqlib_test.py +15 -12
- crosshair/libimpl/iolib.py +7 -4
- crosshair/libimpl/ipaddresslib.py +8 -0
- crosshair/libimpl/itertoolslib_test.py +1 -1
- crosshair/libimpl/mathlib.py +165 -2
- crosshair/libimpl/mathlib_ch_test.py +44 -0
- crosshair/libimpl/mathlib_test.py +59 -16
- crosshair/libimpl/oslib.py +7 -0
- crosshair/libimpl/pathliblib_test.py +10 -0
- crosshair/libimpl/randomlib.py +1 -0
- crosshair/libimpl/randomlib_test.py +6 -4
- crosshair/libimpl/relib.py +180 -59
- crosshair/libimpl/relib_ch_test.py +26 -2
- crosshair/libimpl/relib_test.py +77 -14
- crosshair/libimpl/timelib.py +35 -13
- crosshair/libimpl/timelib_test.py +13 -3
- crosshair/libimpl/typeslib.py +15 -0
- crosshair/libimpl/typeslib_test.py +36 -0
- crosshair/libimpl/unicodedatalib_test.py +3 -3
- crosshair/libimpl/weakreflib.py +13 -0
- crosshair/libimpl/weakreflib_test.py +69 -0
- crosshair/libimpl/zliblib.py +15 -0
- crosshair/libimpl/zliblib_test.py +13 -0
- crosshair/lsp_server.py +21 -10
- crosshair/main.py +48 -28
- crosshair/main_test.py +59 -14
- crosshair/objectproxy.py +39 -14
- crosshair/objectproxy_test.py +27 -13
- crosshair/opcode_intercept.py +212 -24
- crosshair/opcode_intercept_test.py +172 -18
- crosshair/options.py +0 -1
- crosshair/patch_equivalence_test.py +5 -21
- crosshair/path_cover.py +7 -5
- crosshair/path_search.py +6 -4
- crosshair/path_search_test.py +1 -2
- crosshair/pathing_oracle.py +53 -10
- crosshair/pathing_oracle_test.py +21 -0
- crosshair/pure_importer_test.py +5 -21
- crosshair/register_contract.py +16 -6
- crosshair/register_contract_test.py +2 -14
- crosshair/simplestructs.py +154 -85
- crosshair/simplestructs_test.py +16 -2
- crosshair/smtlib.py +24 -0
- crosshair/smtlib_test.py +14 -0
- crosshair/statespace.py +319 -196
- crosshair/statespace_test.py +45 -0
- crosshair/stubs_parser.py +0 -2
- crosshair/test_util.py +87 -25
- crosshair/test_util_test.py +26 -0
- crosshair/tools/check_init_and_setup_coincide.py +0 -3
- crosshair/tools/generate_demo_table.py +2 -2
- crosshair/tracers.py +141 -49
- crosshair/type_repo.py +11 -4
- crosshair/unicode_categories.py +1 -0
- crosshair/util.py +158 -76
- crosshair/util_test.py +13 -20
- crosshair/watcher.py +4 -4
- crosshair/z3util.py +1 -1
- {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/METADATA +45 -36
- crosshair_tool-0.0.100.dist-info/RECORD +176 -0
- {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/WHEEL +2 -1
- crosshair/examples/hypothesis/__init__.py +0 -2
- crosshair/examples/hypothesis/bugs_detected/simple_strategies.py +0 -74
- crosshair_tool-0.0.56.dist-info/RECORD +0 -152
- /crosshair/{examples/hypothesis/bugs_detected/__init__.py → py.typed} +0 -0
- {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/entry_points.txt +0 -0
- {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info/licenses}/LICENSE +0 -0
- {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/top_level.txt +0 -0
crosshair/statespace.py
CHANGED
|
@@ -10,6 +10,7 @@ from collections import Counter, defaultdict
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from sys import _getframe
|
|
12
12
|
from time import monotonic
|
|
13
|
+
from traceback import extract_stack, format_tb
|
|
13
14
|
from types import FrameType
|
|
14
15
|
from typing import (
|
|
15
16
|
Any,
|
|
@@ -17,6 +18,7 @@ from typing import (
|
|
|
17
18
|
Dict,
|
|
18
19
|
List,
|
|
19
20
|
NewType,
|
|
21
|
+
NoReturn,
|
|
20
22
|
Optional,
|
|
21
23
|
Sequence,
|
|
22
24
|
Set,
|
|
@@ -29,18 +31,22 @@ import z3 # type: ignore
|
|
|
29
31
|
|
|
30
32
|
from crosshair import dynamic_typing
|
|
31
33
|
from crosshair.condition_parser import ConditionExpr
|
|
34
|
+
from crosshair.smtlib import parse_smtlib_literal
|
|
32
35
|
from crosshair.tracers import NoTracing, ResumedTracing, is_tracing
|
|
33
36
|
from crosshair.util import (
|
|
34
|
-
|
|
37
|
+
CROSSHAIR_EXTRA_ASSERTS,
|
|
38
|
+
CrossHairInternal,
|
|
35
39
|
IgnoreAttempt,
|
|
40
|
+
NotDeterministic,
|
|
36
41
|
PathTimeout,
|
|
37
42
|
UnknownSatisfiability,
|
|
43
|
+
assert_tracing,
|
|
44
|
+
ch_stack,
|
|
38
45
|
debug,
|
|
39
46
|
in_debug,
|
|
40
47
|
name_of_type,
|
|
41
|
-
test_stack,
|
|
42
48
|
)
|
|
43
|
-
from crosshair.z3util import z3Aassert, z3Not, z3PopNot
|
|
49
|
+
from crosshair.z3util import z3Aassert, z3Not, z3Or, z3PopNot
|
|
44
50
|
|
|
45
51
|
|
|
46
52
|
@functools.total_ordering
|
|
@@ -159,14 +165,19 @@ def model_value_to_python(value: z3.ExprRef) -> object:
|
|
|
159
165
|
if value.num_args() == 1:
|
|
160
166
|
ret.append(model_value_to_python(value.arg(0)))
|
|
161
167
|
return ret
|
|
162
|
-
elif z3.
|
|
168
|
+
elif z3.is_fp(value):
|
|
169
|
+
return parse_smtlib_literal(value.sexpr())
|
|
170
|
+
elif hasattr(value, "py_value"):
|
|
171
|
+
# TODO: how many other cases could be handled with py_value nowadays?
|
|
172
|
+
return value.py_value()
|
|
173
|
+
elif z3.is_int(value): # catch for older z3 versions that don't have py_value
|
|
163
174
|
return value.as_long()
|
|
164
175
|
else:
|
|
165
176
|
return ast.literal_eval(repr(value))
|
|
166
177
|
|
|
167
178
|
|
|
179
|
+
@assert_tracing(False)
|
|
168
180
|
def prefer_true(v: Any) -> bool:
|
|
169
|
-
assert not is_tracing()
|
|
170
181
|
if not (hasattr(v, "var") and z3.is_bool(v.var)):
|
|
171
182
|
with ResumedTracing():
|
|
172
183
|
v = v.__bool__()
|
|
@@ -176,6 +187,22 @@ def prefer_true(v: Any) -> bool:
|
|
|
176
187
|
return space.choose_possible(v.var, probability_true=1.0)
|
|
177
188
|
|
|
178
189
|
|
|
190
|
+
def force_true(v: Any) -> None:
|
|
191
|
+
with NoTracing():
|
|
192
|
+
if not (hasattr(v, "var") and z3.is_bool(v.var)):
|
|
193
|
+
with ResumedTracing():
|
|
194
|
+
v = v.__bool__()
|
|
195
|
+
if not (hasattr(v, "var")):
|
|
196
|
+
raise CrossHairInternal(
|
|
197
|
+
"Attempted to call assert_true on a non-symbolic"
|
|
198
|
+
)
|
|
199
|
+
space = context_statespace()
|
|
200
|
+
# TODO: we can improve this by making a new kind of (unary) assertion node
|
|
201
|
+
# that would not create these useless forks when the space is near exhaustion.
|
|
202
|
+
if not space.choose_possible(v.var, probability_true=1.0):
|
|
203
|
+
raise IgnoreAttempt
|
|
204
|
+
|
|
205
|
+
|
|
179
206
|
class StateSpaceCounter(Counter):
|
|
180
207
|
@property
|
|
181
208
|
def iterations(self) -> int:
|
|
@@ -194,12 +221,8 @@ class StateSpaceCounter(Counter):
|
|
|
194
221
|
return "{" + ", ".join(parts) + "}"
|
|
195
222
|
|
|
196
223
|
|
|
197
|
-
class NotDeterministic(Exception):
|
|
198
|
-
pass
|
|
199
|
-
|
|
200
|
-
|
|
201
224
|
class AbstractPathingOracle:
|
|
202
|
-
def pre_path_hook(self,
|
|
225
|
+
def pre_path_hook(self, space: "StateSpace") -> None:
|
|
203
226
|
pass
|
|
204
227
|
|
|
205
228
|
def post_path_hook(self, path: Sequence["SearchTreeNode"]) -> None:
|
|
@@ -225,14 +248,16 @@ class StateSpaceContext:
|
|
|
225
248
|
|
|
226
249
|
def __enter__(self):
|
|
227
250
|
prev = real_getattr(_THREAD_LOCALS, "space", None)
|
|
228
|
-
|
|
251
|
+
if prev is not None:
|
|
252
|
+
raise CrossHairInternal("Already in a state space context")
|
|
229
253
|
space = self.space
|
|
230
254
|
_THREAD_LOCALS.space = space
|
|
231
255
|
space.mark_all_parent_frames()
|
|
232
256
|
|
|
233
257
|
def __exit__(self, exc_type, exc_value, tb):
|
|
234
258
|
prev = real_getattr(_THREAD_LOCALS, "space", None)
|
|
235
|
-
|
|
259
|
+
if prev is not self.space:
|
|
260
|
+
raise CrossHairInternal("State space was altered in context")
|
|
236
261
|
_THREAD_LOCALS.space = None
|
|
237
262
|
return False
|
|
238
263
|
|
|
@@ -244,7 +269,7 @@ def optional_context_statespace() -> Optional["StateSpace"]:
|
|
|
244
269
|
def context_statespace() -> "StateSpace":
|
|
245
270
|
space = _THREAD_LOCALS.space
|
|
246
271
|
if space is None:
|
|
247
|
-
raise
|
|
272
|
+
raise CrossHairInternal("Not in a statespace context")
|
|
248
273
|
return space
|
|
249
274
|
|
|
250
275
|
|
|
@@ -268,59 +293,27 @@ class NodeLike:
|
|
|
268
293
|
"""
|
|
269
294
|
raise NotImplementedError
|
|
270
295
|
|
|
271
|
-
def is_stem(self) -> bool:
|
|
272
|
-
return False
|
|
273
|
-
|
|
274
|
-
def grow_into(self, node: _N) -> _N:
|
|
275
|
-
raise NotImplementedError
|
|
276
|
-
|
|
277
|
-
def simplify(self) -> "NodeLike":
|
|
278
|
-
return self
|
|
279
|
-
|
|
280
296
|
def stats(self) -> StateSpaceCounter:
|
|
281
297
|
raise NotImplementedError
|
|
282
298
|
|
|
299
|
+
children_fields: Tuple[str, ...] = ()
|
|
283
300
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
return (
|
|
292
|
-
CallAnalysis(VerificationStatus.UNKNOWN)
|
|
293
|
-
if self.evolution is None
|
|
294
|
-
else self.evolution.get_result()
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
def is_stem(self) -> bool:
|
|
298
|
-
return self.evolution is None
|
|
299
|
-
|
|
300
|
-
def grow_into(self, node: _N) -> _N:
|
|
301
|
-
self.evolution = node
|
|
302
|
-
return node
|
|
303
|
-
|
|
304
|
-
def simplify(self):
|
|
305
|
-
return self if self.evolution is None else self.evolution
|
|
306
|
-
|
|
307
|
-
def stats(self) -> StateSpaceCounter:
|
|
308
|
-
return StateSpaceCounter() if self.evolution is None else self.evolution.stats()
|
|
309
|
-
|
|
310
|
-
def __repr__(self) -> str:
|
|
311
|
-
return "NodeStem()"
|
|
301
|
+
def replace_child(self, current_child, replacement_child: "NodeLike") -> None:
|
|
302
|
+
for child_field in self.children_fields:
|
|
303
|
+
child = getattr(self, child_field)
|
|
304
|
+
if child is current_child:
|
|
305
|
+
setattr(self, child_field, replacement_child)
|
|
306
|
+
return
|
|
307
|
+
raise CrossHairInternal(f"Child {current_child} not found in {self}")
|
|
312
308
|
|
|
313
309
|
|
|
314
310
|
class SearchTreeNode(NodeLike):
|
|
315
|
-
"""
|
|
316
|
-
Represent a single decision point.
|
|
317
|
-
|
|
318
|
-
Abstract helper class for StateSpace.
|
|
319
|
-
"""
|
|
311
|
+
"""A node in the execution path tree."""
|
|
320
312
|
|
|
321
313
|
stacktail: Tuple[str, ...] = ()
|
|
322
314
|
result: CallAnalysis = CallAnalysis()
|
|
323
315
|
exhausted: bool = False
|
|
316
|
+
iteration: Optional[int] = None
|
|
324
317
|
|
|
325
318
|
def choose(
|
|
326
319
|
self, space: "StateSpace", probability_true: Optional[float] = None
|
|
@@ -345,13 +338,34 @@ class SearchTreeNode(NodeLike):
|
|
|
345
338
|
raise NotImplementedError
|
|
346
339
|
|
|
347
340
|
|
|
348
|
-
|
|
349
|
-
|
|
341
|
+
class NodeStem(NodeLike):
|
|
342
|
+
def __init__(self, parent: SearchTreeNode, parent_attr_name: str):
|
|
343
|
+
self.parent = parent
|
|
344
|
+
self.parent_attr_name = parent_attr_name
|
|
345
|
+
|
|
346
|
+
def is_exhausted(self) -> bool:
|
|
347
|
+
return False
|
|
348
|
+
|
|
349
|
+
def get_result(self) -> CallAnalysis:
|
|
350
|
+
return CallAnalysis(VerificationStatus.UNKNOWN)
|
|
351
|
+
|
|
352
|
+
def stats(self) -> StateSpaceCounter:
|
|
353
|
+
return StateSpaceCounter()
|
|
354
|
+
|
|
355
|
+
def __repr__(self) -> str:
|
|
356
|
+
return "NodeStem()"
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def solver_is_sat(solver, *exprs) -> bool:
|
|
360
|
+
ret = solver.check(*exprs)
|
|
350
361
|
if ret == z3.unknown:
|
|
351
|
-
debug("Z3
|
|
362
|
+
debug("Z3 Unknown satisfiability. Reason:", solver.reason_unknown())
|
|
363
|
+
debug("Call stack at time of unknown sat:", ch_stack())
|
|
352
364
|
if solver.reason_unknown() == "interrupted from keyboard":
|
|
353
365
|
raise KeyboardInterrupt
|
|
354
|
-
|
|
366
|
+
if exprs:
|
|
367
|
+
debug("While attempting to assert\n", *(e.sexpr() for e in exprs))
|
|
368
|
+
debug("Solver state follows:\n", solver.sexpr())
|
|
355
369
|
raise UnknownSatisfiability
|
|
356
370
|
return ret == z3.sat
|
|
357
371
|
|
|
@@ -390,7 +404,7 @@ class SinglePathNode(SearchTreeNode):
|
|
|
390
404
|
|
|
391
405
|
def __init__(self, decision: bool):
|
|
392
406
|
self.decision = decision
|
|
393
|
-
self.child = NodeStem()
|
|
407
|
+
self.child = NodeStem(self, "child")
|
|
394
408
|
self._random = newrandom()
|
|
395
409
|
|
|
396
410
|
def choose(
|
|
@@ -399,12 +413,14 @@ class SinglePathNode(SearchTreeNode):
|
|
|
399
413
|
return (self.decision, 1.0, self.child)
|
|
400
414
|
|
|
401
415
|
def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
|
|
402
|
-
self.child
|
|
416
|
+
assert isinstance(self.child, SearchTreeNode)
|
|
403
417
|
return (self.child.get_result(), self.child.is_exhausted())
|
|
404
418
|
|
|
405
419
|
def stats(self) -> StateSpaceCounter:
|
|
406
420
|
return self.child.stats()
|
|
407
421
|
|
|
422
|
+
children_fields = ("child",)
|
|
423
|
+
|
|
408
424
|
|
|
409
425
|
class BranchCounter:
|
|
410
426
|
__slots__ = ["pos_ct", "neg_ct"]
|
|
@@ -424,10 +440,11 @@ class RootNode(SinglePathNode):
|
|
|
424
440
|
)
|
|
425
441
|
from crosshair.pathing_oracle import CoveragePathingOracle # circular import
|
|
426
442
|
|
|
427
|
-
self.pathing_oracle = CoveragePathingOracle()
|
|
443
|
+
self.pathing_oracle: AbstractPathingOracle = CoveragePathingOracle()
|
|
444
|
+
self.iteration = 0
|
|
428
445
|
|
|
429
446
|
|
|
430
|
-
class
|
|
447
|
+
class DetachedPathNode(SinglePathNode):
|
|
431
448
|
def __init__(self):
|
|
432
449
|
super().__init__(True)
|
|
433
450
|
# Seems like `exhausted` should be True, but we set to False until we can
|
|
@@ -437,7 +454,6 @@ class DeatchedPathNode(SinglePathNode):
|
|
|
437
454
|
self._stats = None
|
|
438
455
|
|
|
439
456
|
def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
|
|
440
|
-
self.child = self.child.simplify()
|
|
441
457
|
return (leaf_analysis, True)
|
|
442
458
|
|
|
443
459
|
def stats(self) -> StateSpaceCounter:
|
|
@@ -467,13 +483,15 @@ class BinaryPathNode(SearchTreeNode):
|
|
|
467
483
|
def stats(self) -> StateSpaceCounter:
|
|
468
484
|
return self._stats
|
|
469
485
|
|
|
486
|
+
children_fields = ("negative", "positive")
|
|
487
|
+
|
|
470
488
|
|
|
471
489
|
class RandomizedBinaryPathNode(BinaryPathNode):
|
|
472
490
|
def __init__(self, rand: random.Random):
|
|
473
491
|
super().__init__()
|
|
474
492
|
self._random = rand
|
|
475
|
-
self.positive = NodeStem()
|
|
476
|
-
self.negative = NodeStem()
|
|
493
|
+
self.positive = NodeStem(self, "positive")
|
|
494
|
+
self.negative = NodeStem(self, "negative")
|
|
477
495
|
|
|
478
496
|
def probability_true(
|
|
479
497
|
self, space: "StateSpace", requested_probability: Optional[float] = None
|
|
@@ -498,10 +516,6 @@ class RandomizedBinaryPathNode(BinaryPathNode):
|
|
|
498
516
|
else:
|
|
499
517
|
return (positive_ok, 1.0, self.positive if positive_ok else self.negative)
|
|
500
518
|
|
|
501
|
-
def _simplify(self) -> None:
|
|
502
|
-
self.positive = self.positive.simplify()
|
|
503
|
-
self.negative = self.negative.simplify()
|
|
504
|
-
|
|
505
519
|
|
|
506
520
|
class ParallelNode(RandomizedBinaryPathNode):
|
|
507
521
|
"""Choose either path; the first complete result will be used."""
|
|
@@ -515,7 +529,6 @@ class ParallelNode(RandomizedBinaryPathNode):
|
|
|
515
529
|
return f"ParallelNode(false_pct={self._false_probability}, {self._desc})"
|
|
516
530
|
|
|
517
531
|
def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
|
|
518
|
-
self._simplify()
|
|
519
532
|
positive, negative = self.positive, self.negative
|
|
520
533
|
pos_exhausted = positive.is_exhausted()
|
|
521
534
|
neg_exhausted = negative.is_exhausted()
|
|
@@ -592,13 +605,11 @@ class WorstResultNode(RandomizedBinaryPathNode):
|
|
|
592
605
|
if not solver_is_sat(solver, expr):
|
|
593
606
|
self.forced_path = False
|
|
594
607
|
else:
|
|
595
|
-
#
|
|
596
|
-
|
|
597
|
-
_PERFORM_EXTRA_SAT_CHECKS = True
|
|
598
|
-
if _PERFORM_EXTRA_SAT_CHECKS and not solver_is_sat(solver, expr):
|
|
608
|
+
# We run into soundness issues on occasion:
|
|
609
|
+
if CROSSHAIR_EXTRA_ASSERTS and not solver_is_sat(solver, expr):
|
|
599
610
|
debug(" *** Reached impossible code path *** ")
|
|
600
611
|
debug("Current solver state:\n", str(solver))
|
|
601
|
-
raise
|
|
612
|
+
raise CrossHairInternal("Reached impossible code path")
|
|
602
613
|
self.forced_path = True
|
|
603
614
|
self.expr = expr
|
|
604
615
|
|
|
@@ -635,7 +646,6 @@ class WorstResultNode(RandomizedBinaryPathNode):
|
|
|
635
646
|
)
|
|
636
647
|
|
|
637
648
|
def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
|
|
638
|
-
self._simplify()
|
|
639
649
|
positive, negative = self.positive, self.negative
|
|
640
650
|
exhausted = self._is_exhausted()
|
|
641
651
|
if node_status(positive) == VerificationStatus.REFUTED or (
|
|
@@ -660,7 +670,7 @@ class ModelValueNode(WorstResultNode):
|
|
|
660
670
|
def __init__(self, rand: random.Random, expr: z3.ExprRef, solver: z3.Solver):
|
|
661
671
|
if not solver_is_sat(solver):
|
|
662
672
|
debug("Solver unexpectedly unsat; solver state:", solver.sexpr())
|
|
663
|
-
raise
|
|
673
|
+
raise CrossHairInternal("Unexpected unsat from solver")
|
|
664
674
|
|
|
665
675
|
self.condition_value = solver.model().evaluate(expr, model_completion=True)
|
|
666
676
|
self._stats_key = f"realize_{expr}" if z3.is_const(expr) else None
|
|
@@ -677,7 +687,6 @@ class ModelValueNode(WorstResultNode):
|
|
|
677
687
|
|
|
678
688
|
def debug_path_tree(node, highlights, prefix="") -> List[str]:
|
|
679
689
|
highlighted = node in highlights
|
|
680
|
-
node = node.simplify()
|
|
681
690
|
highlighted |= node in highlights
|
|
682
691
|
if isinstance(node, BinaryPathNode):
|
|
683
692
|
if isinstance(node, WorstResultNode) and node.forced_path is not None:
|
|
@@ -710,6 +719,17 @@ def debug_path_tree(node, highlights, prefix="") -> List[str]:
|
|
|
710
719
|
return [f"{prefix} -> {str(node)} {node.stats()}"]
|
|
711
720
|
|
|
712
721
|
|
|
722
|
+
def make_default_solver() -> z3.Solver:
|
|
723
|
+
"""Create a new solver with default settings."""
|
|
724
|
+
smt_tactic = z3.Tactic("smt")
|
|
725
|
+
solver = smt_tactic.solver()
|
|
726
|
+
solver.set("mbqi", True)
|
|
727
|
+
# turn off every randomization thing we can think of:
|
|
728
|
+
solver.set("random-seed", 42)
|
|
729
|
+
solver.set("smt.random-seed", 42)
|
|
730
|
+
return solver
|
|
731
|
+
|
|
732
|
+
|
|
713
733
|
class StateSpace:
|
|
714
734
|
"""Holds various information about the SMT solver's current state."""
|
|
715
735
|
|
|
@@ -723,16 +743,12 @@ class StateSpace:
|
|
|
723
743
|
model_check_timeout: float,
|
|
724
744
|
search_root: RootNode,
|
|
725
745
|
):
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
# turn off every randomization thing we can think of:
|
|
733
|
-
self.solver.set("random-seed", 42)
|
|
734
|
-
self.solver.set("smt.random-seed", 42)
|
|
735
|
-
# self.solver.set('randomize', False)
|
|
746
|
+
self.solver = make_default_solver()
|
|
747
|
+
if model_check_timeout < 1 << 63:
|
|
748
|
+
self.smt_timeout: Optional[int] = int(model_check_timeout * 1000 + 1)
|
|
749
|
+
self.solver.set(timeout=self.smt_timeout)
|
|
750
|
+
else:
|
|
751
|
+
self.smt_timeout = None
|
|
736
752
|
self.choices_made: List[SearchTreeNode] = []
|
|
737
753
|
self.status_cap: Optional[VerificationStatus] = None
|
|
738
754
|
self.heaps: List[List[Tuple[z3.ExprRef, Type, object]]] = [[]]
|
|
@@ -747,17 +763,30 @@ class StateSpace:
|
|
|
747
763
|
self._random = search_root._random
|
|
748
764
|
_, _, self._search_position = search_root.choose(self)
|
|
749
765
|
self._deferred_assumptions = []
|
|
750
|
-
search_root.
|
|
766
|
+
assert search_root.iteration is not None
|
|
767
|
+
search_root.iteration += 1
|
|
768
|
+
search_root.pathing_oracle.pre_path_hook(self)
|
|
751
769
|
|
|
752
|
-
def add(self, expr
|
|
770
|
+
def add(self, expr) -> None:
|
|
753
771
|
with NoTracing():
|
|
772
|
+
if hasattr(expr, "var"):
|
|
773
|
+
expr = expr.var
|
|
774
|
+
elif not isinstance(expr, z3.ExprRef):
|
|
775
|
+
if type(expr) is bool:
|
|
776
|
+
raise CrossHairInternal(
|
|
777
|
+
"Attempted to assert a concrete boolean (look for unexpected realization)"
|
|
778
|
+
)
|
|
779
|
+
raise CrossHairInternal(
|
|
780
|
+
"Expected symbolic boolean, but supplied expression of type",
|
|
781
|
+
name_of_type(type(expr)),
|
|
782
|
+
)
|
|
754
783
|
# debug('Committed to ', expr)
|
|
755
784
|
already_known = self._exprs_known.get(expr)
|
|
756
785
|
if already_known is None:
|
|
757
786
|
self.solver.add(expr)
|
|
758
787
|
self._exprs_known[expr] = True
|
|
759
788
|
elif already_known is not True:
|
|
760
|
-
raise
|
|
789
|
+
raise CrossHairInternal
|
|
761
790
|
|
|
762
791
|
def rand(self) -> random.Random:
|
|
763
792
|
return self._random
|
|
@@ -771,31 +800,42 @@ class StateSpace:
|
|
|
771
800
|
return value # type: ignore
|
|
772
801
|
|
|
773
802
|
def stats_lookahead(self) -> Tuple[StateSpaceCounter, StateSpaceCounter]:
|
|
774
|
-
node = self._search_position
|
|
775
|
-
if node
|
|
803
|
+
node = self._search_position
|
|
804
|
+
if isinstance(node, NodeStem):
|
|
776
805
|
return (StateSpaceCounter(), StateSpaceCounter())
|
|
777
|
-
assert isinstance(
|
|
778
|
-
node, BinaryPathNode
|
|
779
|
-
), f"node {node} {node.is_stem()} is not a binarypathnode"
|
|
806
|
+
assert isinstance(node, BinaryPathNode), f"node {node} is not a binarypathnode"
|
|
780
807
|
return node.stats_lookahead()
|
|
781
808
|
|
|
809
|
+
def grow_into(self, node: _N) -> _N:
|
|
810
|
+
assert isinstance(self._search_position, NodeStem)
|
|
811
|
+
self._search_position.parent.replace_child(self._search_position, node)
|
|
812
|
+
node.iteration = self._root.iteration
|
|
813
|
+
self._search_position = node
|
|
814
|
+
return node
|
|
815
|
+
|
|
782
816
|
def fork_parallel(self, false_probability: float, desc: str = "") -> bool:
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
)
|
|
817
|
+
node = self._search_position
|
|
818
|
+
if isinstance(node, NodeStem):
|
|
819
|
+
node = self.grow_into(ParallelNode(self._random, false_probability, desc))
|
|
820
|
+
node.stacktail = self.gen_stack_descriptions()
|
|
787
821
|
assert isinstance(node, ParallelNode)
|
|
788
822
|
self._search_position = node
|
|
789
823
|
else:
|
|
790
|
-
|
|
791
|
-
|
|
824
|
+
if not isinstance(node, ParallelNode):
|
|
825
|
+
self.raise_not_deterministic(
|
|
826
|
+
node, "Wrong node type (expected ParallelNode)"
|
|
827
|
+
)
|
|
792
828
|
node._false_probability = false_probability
|
|
793
829
|
self.choices_made.append(node)
|
|
794
830
|
ret, _, next_node = node.choose(self)
|
|
795
831
|
self._search_position = next_node
|
|
796
832
|
return ret
|
|
797
833
|
|
|
798
|
-
def is_possible(self, expr
|
|
834
|
+
def is_possible(self, expr) -> bool:
|
|
835
|
+
with NoTracing():
|
|
836
|
+
if hasattr(expr, "var"):
|
|
837
|
+
expr = expr.var
|
|
838
|
+
debug("is possible?", expr)
|
|
799
839
|
return solver_is_sat(self.solver, expr)
|
|
800
840
|
|
|
801
841
|
def mark_all_parent_frames(self):
|
|
@@ -811,15 +851,7 @@ class StateSpace:
|
|
|
811
851
|
|
|
812
852
|
def gen_stack_descriptions(self) -> Tuple[str, ...]:
|
|
813
853
|
f: Any = _getframe().f_back.f_back # type: ignore
|
|
814
|
-
|
|
815
|
-
# frames = [f := f.f_back or f for _ in range(8)]
|
|
816
|
-
frames = []
|
|
817
|
-
external_frame_ids = self.external_frame_ids
|
|
818
|
-
for _ in range(8):
|
|
819
|
-
if id(f) in external_frame_ids:
|
|
820
|
-
break
|
|
821
|
-
frames.append(f)
|
|
822
|
-
f = f.f_back
|
|
854
|
+
frames = [f := f.f_back or f for _ in range(8)]
|
|
823
855
|
# TODO: To help oracles, I'd like to add sub-line resolution via f.f_lasti;
|
|
824
856
|
# however, in Python >= 3.11, the instruction pointer can shift between
|
|
825
857
|
# PRECALL and CALL opcodes, triggering our nondeterminism check.
|
|
@@ -834,66 +866,45 @@ class StateSpace:
|
|
|
834
866
|
)
|
|
835
867
|
raise PathTimeout
|
|
836
868
|
|
|
869
|
+
@assert_tracing(False)
|
|
837
870
|
def choose_possible(
|
|
838
871
|
self, expr: z3.ExprRef, probability_true: Optional[float] = None
|
|
839
872
|
) -> bool:
|
|
840
873
|
known_result = self._exprs_known.get(expr)
|
|
841
874
|
if isinstance(known_result, bool):
|
|
842
875
|
return known_result
|
|
843
|
-
if is_tracing():
|
|
844
|
-
raise CrosshairInternal
|
|
845
|
-
if self._search_position.is_stem():
|
|
846
|
-
# We only allow time outs at stems - that's because we don't want
|
|
847
|
-
# to think about how mutating an existing path branch would work:
|
|
848
|
-
self.check_timeout()
|
|
849
|
-
node = self._search_position.grow_into(
|
|
850
|
-
WorstResultNode(self._random, expr, self.solver)
|
|
851
|
-
)
|
|
852
|
-
else:
|
|
853
|
-
node = self._search_position.simplify() # type: ignore
|
|
854
|
-
if not isinstance(node, WorstResultNode):
|
|
855
|
-
debug(" *** Begin Not Deterministic Debug *** ")
|
|
856
|
-
debug(" Traceback: ", test_stack())
|
|
857
|
-
debug("Decision expression:")
|
|
858
|
-
debug(f" {expr}")
|
|
859
|
-
debug("Now found at incompatible node of type:")
|
|
860
|
-
debug(f" {type(node)}")
|
|
861
|
-
debug(" *** End Not Deterministic Debug *** ")
|
|
862
|
-
raise NotDeterministic
|
|
863
|
-
if not z3.eq(node.expr, expr):
|
|
864
|
-
debug(" *** Begin Not Deterministic Debug *** ")
|
|
865
|
-
debug(" Traceback: ", test_stack())
|
|
866
|
-
debug("Decision expression changed from:")
|
|
867
|
-
debug(f" {node.expr}")
|
|
868
|
-
debug("To:")
|
|
869
|
-
debug(f" {expr}")
|
|
870
|
-
debug(" *** End Not Deterministic Debug *** ")
|
|
871
|
-
raise NotDeterministic
|
|
872
|
-
|
|
873
|
-
self._search_position = node
|
|
874
876
|
# NOTE: format_stack() is more human readable, but it pulls source file contents,
|
|
875
877
|
# so it is (1) slow, and (2) unstable when source code changes while we are checking.
|
|
876
878
|
stacktail = self.gen_stack_descriptions()
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
879
|
+
if isinstance(self._search_position, SearchTreeNode):
|
|
880
|
+
node = self._search_position
|
|
881
|
+
not_deterministic_reason = (
|
|
882
|
+
(
|
|
883
|
+
(not isinstance(node, WorstResultNode))
|
|
884
|
+
and f"Wrong node type (is {name_of_type(type(node))}, expected WorstResultNode)"
|
|
885
|
+
)
|
|
886
|
+
# TODO: Not clear whether we want this stack trace check.
|
|
887
|
+
# A stack change usually indicates a serious problem, but not 100% of the time.
|
|
888
|
+
# Keeping it would mean that we fail earlier.
|
|
889
|
+
# But also see https://github.com/HypothesisWorks/hypothesis/pull/4034#issuecomment-2606415404
|
|
890
|
+
# or (node.stacktail != stacktail and "Stack trace changed")
|
|
891
|
+
or (
|
|
892
|
+
(hasattr(node, "expr") and (not z3.eq(node.expr, expr)))
|
|
893
|
+
and "SMT expression changed"
|
|
894
|
+
)
|
|
895
|
+
)
|
|
896
|
+
if not_deterministic_reason:
|
|
897
|
+
self.raise_not_deterministic(
|
|
898
|
+
node, not_deterministic_reason, expr=expr, stacktail=stacktail
|
|
899
|
+
)
|
|
880
900
|
else:
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
debug(stacktail)
|
|
887
|
-
debug(" Decision points prior to this:")
|
|
888
|
-
for choice in self.choices_made:
|
|
889
|
-
debug(" ", choice)
|
|
890
|
-
debug(" Stack Diff: ")
|
|
891
|
-
import difflib
|
|
892
|
-
|
|
893
|
-
debug("\n".join(difflib.context_diff(node.stacktail, stacktail)))
|
|
894
|
-
debug(" *** End Not Deterministic Debug *** ")
|
|
895
|
-
raise NotDeterministic
|
|
901
|
+
# We only allow time outs at stems - that's because we don't want
|
|
902
|
+
# to think about how mutating an existing path branch would work:
|
|
903
|
+
self.check_timeout()
|
|
904
|
+
node = self.grow_into(WorstResultNode(self._random, expr, self.solver))
|
|
905
|
+
node.stacktail = stacktail
|
|
896
906
|
|
|
907
|
+
self._search_position = node
|
|
897
908
|
choose_true, chosen_probability, stem = node.choose(
|
|
898
909
|
self, probability_true=probability_true
|
|
899
910
|
)
|
|
@@ -909,32 +920,64 @@ class StateSpace:
|
|
|
909
920
|
chosen_expr = expr if choose_true else z3Not(expr)
|
|
910
921
|
if in_debug():
|
|
911
922
|
debug(
|
|
912
|
-
"SMT chose:",
|
|
913
|
-
|
|
914
|
-
"(chance:",
|
|
915
|
-
chosen_probability,
|
|
916
|
-
")",
|
|
923
|
+
f"SMT chose: {chosen_expr} (chance: {chosen_probability}) at",
|
|
924
|
+
ch_stack(),
|
|
917
925
|
)
|
|
918
926
|
z3Aassert(self.solver, chosen_expr)
|
|
919
927
|
self._exprs_known[expr] = choose_true
|
|
920
928
|
return choose_true
|
|
921
929
|
|
|
930
|
+
def raise_not_deterministic(
|
|
931
|
+
self,
|
|
932
|
+
node: NodeLike,
|
|
933
|
+
reason: str,
|
|
934
|
+
expr: Optional[z3.ExprRef] = None,
|
|
935
|
+
stacktail: Optional[Tuple[str, ...]] = None,
|
|
936
|
+
currently_handling: Optional[BaseException] = None,
|
|
937
|
+
) -> NoReturn:
|
|
938
|
+
lines = ["*** Begin Not Deterministic Debug ***"]
|
|
939
|
+
if getattr(node, "iteration", None) is not None:
|
|
940
|
+
lines.append(f"Previous iteration: {node.iteration}") # type: ignore
|
|
941
|
+
if hasattr(node, "expr"):
|
|
942
|
+
lines.append(f"Previous SMT expression: {node.expr}") # type: ignore
|
|
943
|
+
if expr is not None:
|
|
944
|
+
lines.append(f"Current SMT expression: {expr}")
|
|
945
|
+
if not stacktail:
|
|
946
|
+
if currently_handling is not None:
|
|
947
|
+
stacktail = tuple(format_tb(currently_handling.__traceback__))
|
|
948
|
+
else:
|
|
949
|
+
stacktail = self.gen_stack_descriptions()
|
|
950
|
+
lines.append("Current stack tail:")
|
|
951
|
+
lines.extend(f" {x}" for x in stacktail)
|
|
952
|
+
if hasattr(node, "stacktail"):
|
|
953
|
+
lines.append("Previous stack tail:")
|
|
954
|
+
lines.extend(f" {x}" for x in node.stacktail)
|
|
955
|
+
lines.append(f"Reason: {reason}")
|
|
956
|
+
lines.append("*** End Not Deterministic Debug ***")
|
|
957
|
+
for line in lines:
|
|
958
|
+
print(line)
|
|
959
|
+
exc = NotDeterministic()
|
|
960
|
+
if currently_handling:
|
|
961
|
+
raise exc from currently_handling
|
|
962
|
+
else:
|
|
963
|
+
raise exc
|
|
964
|
+
|
|
922
965
|
def find_model_value(self, expr: z3.ExprRef) -> Any:
|
|
923
966
|
with NoTracing():
|
|
924
967
|
while True:
|
|
925
|
-
if self._search_position
|
|
926
|
-
self._search_position = self.
|
|
968
|
+
if isinstance(self._search_position, NodeStem):
|
|
969
|
+
self._search_position = self.grow_into(
|
|
927
970
|
ModelValueNode(self._random, expr, self.solver)
|
|
928
971
|
)
|
|
929
|
-
node = self._search_position
|
|
972
|
+
node = self._search_position
|
|
930
973
|
if isinstance(node, SearchLeaf):
|
|
931
|
-
raise
|
|
974
|
+
raise CrossHairInternal(
|
|
932
975
|
f"Cannot use symbolics; path is already terminated"
|
|
933
976
|
)
|
|
934
977
|
if not isinstance(node, ModelValueNode):
|
|
935
978
|
debug(" *** Begin Not Deterministic Debug *** ")
|
|
936
979
|
debug(f"Model value node expected; found {type(node)} instead.")
|
|
937
|
-
debug(" Traceback: ",
|
|
980
|
+
debug(" Traceback: ", ch_stack())
|
|
938
981
|
debug(" *** End Not Deterministic Debug *** ")
|
|
939
982
|
raise NotDeterministic
|
|
940
983
|
(chosen, _, next_node) = node.choose(self, probability_true=1.0)
|
|
@@ -950,14 +993,14 @@ class StateSpace:
|
|
|
950
993
|
):
|
|
951
994
|
self._already_logged.add(expr)
|
|
952
995
|
debug("SMT realized symbolic:", expr, "==", repr(ret))
|
|
953
|
-
debug("Realized at",
|
|
996
|
+
debug("Realized at", ch_stack())
|
|
954
997
|
return ret
|
|
955
998
|
else:
|
|
956
999
|
self.solver.add(expr != node.condition_value)
|
|
957
1000
|
|
|
958
1001
|
def find_model_value_for_function(self, expr: z3.ExprRef) -> object:
|
|
959
1002
|
if not solver_is_sat(self.solver):
|
|
960
|
-
raise
|
|
1003
|
+
raise CrossHairInternal("model unexpectedly became unsatisfiable")
|
|
961
1004
|
# TODO: this need to go into a tree node that returns UNKNOWN or worse
|
|
962
1005
|
# (because it just returns one example function; it's not covering the space)
|
|
963
1006
|
|
|
@@ -989,7 +1032,7 @@ class StateSpace:
|
|
|
989
1032
|
) -> object:
|
|
990
1033
|
with NoTracing():
|
|
991
1034
|
# TODO: needs more testing
|
|
992
|
-
for
|
|
1035
|
+
for curref, curtyp, curval in self.heaps[snapshot]:
|
|
993
1036
|
|
|
994
1037
|
# TODO: using unify() is almost certainly wrong; just because the types
|
|
995
1038
|
# have some instances in common does not mean that `curval` actually
|
|
@@ -999,7 +1042,7 @@ class StateSpace:
|
|
|
999
1042
|
continue
|
|
1000
1043
|
if self.smt_fork(curref == ref, probability_true=0.1):
|
|
1001
1044
|
debug(
|
|
1002
|
-
"
|
|
1045
|
+
"Heap key lookup ",
|
|
1003
1046
|
ref,
|
|
1004
1047
|
": Found existing. ",
|
|
1005
1048
|
"type:",
|
|
@@ -1010,7 +1053,7 @@ class StateSpace:
|
|
|
1010
1053
|
return curval
|
|
1011
1054
|
ret = proxy_generator(typ)
|
|
1012
1055
|
debug(
|
|
1013
|
-
"
|
|
1056
|
+
"Heap key lookup ",
|
|
1014
1057
|
ref,
|
|
1015
1058
|
": Created new. ",
|
|
1016
1059
|
"type:",
|
|
@@ -1026,6 +1069,51 @@ class StateSpace:
|
|
|
1026
1069
|
self.next_uniq += 1
|
|
1027
1070
|
return "_{:x}".format(self.next_uniq)
|
|
1028
1071
|
|
|
1072
|
+
@assert_tracing(False)
|
|
1073
|
+
def smt_fanout(
|
|
1074
|
+
self,
|
|
1075
|
+
exprs_and_results: Sequence[Tuple[z3.ExprRef, object]],
|
|
1076
|
+
desc: str,
|
|
1077
|
+
weights: Optional[Sequence[float]] = None,
|
|
1078
|
+
none_of_the_above_weight: float = 0.0,
|
|
1079
|
+
):
|
|
1080
|
+
"""Performs a weighted binary search over the given SMT expressions."""
|
|
1081
|
+
exprs = [e for (e, _) in exprs_and_results]
|
|
1082
|
+
final_weights = [1.0] * len(exprs) if weights is None else weights
|
|
1083
|
+
if CROSSHAIR_EXTRA_ASSERTS:
|
|
1084
|
+
if len(final_weights) != len(exprs):
|
|
1085
|
+
raise CrossHairInternal("inconsistent smt_fanout exprs and weights")
|
|
1086
|
+
if not all(0 < w for w in final_weights):
|
|
1087
|
+
raise CrossHairInternal("smt_fanout weights must be greater than zero")
|
|
1088
|
+
if not self.is_possible(z3Or(*exprs)):
|
|
1089
|
+
raise CrossHairInternal(
|
|
1090
|
+
"no smt_fanout option is possible: " + repr(exprs)
|
|
1091
|
+
)
|
|
1092
|
+
if self.is_possible(z3Not(z3Or(*exprs))):
|
|
1093
|
+
raise CrossHairInternal(
|
|
1094
|
+
"smt_fanout options are not exhaustive: " + repr(exprs)
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
def attempt(start: int, end: int):
|
|
1098
|
+
size = end - start
|
|
1099
|
+
if size == 1:
|
|
1100
|
+
return exprs_and_results[start][1]
|
|
1101
|
+
mid = (start + end) // 2
|
|
1102
|
+
left_exprs = exprs[start:mid]
|
|
1103
|
+
left_weight = sum(final_weights[start:mid])
|
|
1104
|
+
right_weight = sum(final_weights[mid:end])
|
|
1105
|
+
if self.smt_fork(
|
|
1106
|
+
z3Or(*left_exprs),
|
|
1107
|
+
probability_true=left_weight / (left_weight + right_weight),
|
|
1108
|
+
desc=f"{desc}_fan_size_{size}",
|
|
1109
|
+
):
|
|
1110
|
+
return attempt(start, mid)
|
|
1111
|
+
else:
|
|
1112
|
+
return attempt(mid, end)
|
|
1113
|
+
|
|
1114
|
+
return attempt(0, len(exprs))
|
|
1115
|
+
|
|
1116
|
+
@assert_tracing(False)
|
|
1029
1117
|
def smt_fork(
|
|
1030
1118
|
self,
|
|
1031
1119
|
expr: Optional[z3.ExprRef] = None,
|
|
@@ -1039,7 +1127,15 @@ class StateSpace:
|
|
|
1039
1127
|
def defer_assumption(self, description: str, checker: Callable[[], bool]) -> None:
|
|
1040
1128
|
self._deferred_assumptions.append((description, checker))
|
|
1041
1129
|
|
|
1042
|
-
def
|
|
1130
|
+
def extend_timeouts(
|
|
1131
|
+
self, constant_factor: float = 0.0, smt_multiple: Optional[float] = None
|
|
1132
|
+
) -> None:
|
|
1133
|
+
self.execution_deadline += constant_factor
|
|
1134
|
+
if self.smt_timeout is not None and smt_multiple is not None:
|
|
1135
|
+
self.smt_timeout = int(self.smt_timeout * smt_multiple)
|
|
1136
|
+
self.solver.set(timeout=self.smt_timeout)
|
|
1137
|
+
|
|
1138
|
+
def detach_path(self, currently_handling: Optional[BaseException] = None) -> None:
|
|
1043
1139
|
"""
|
|
1044
1140
|
Mark the current path exhausted.
|
|
1045
1141
|
|
|
@@ -1052,36 +1148,62 @@ class StateSpace:
|
|
|
1052
1148
|
if self.is_detached:
|
|
1053
1149
|
debug("Path is already detached")
|
|
1054
1150
|
return
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1151
|
+
# Give ourselves a time extension for deferred assumptions and
|
|
1152
|
+
# (likely) counterexample generation to follow.
|
|
1153
|
+
self.extend_timeouts(constant_factor=4.0, smt_multiple=2.0)
|
|
1154
|
+
self.is_detached = True
|
|
1155
|
+
if isinstance(
|
|
1156
|
+
currently_handling,
|
|
1157
|
+
(NotDeterministic, UnknownSatisfiability, PathTimeout),
|
|
1158
|
+
):
|
|
1159
|
+
# These exceptions can happen at any time; we may not be at a stem node.
|
|
1160
|
+
node = DetachedPathNode()
|
|
1161
|
+
self.choices_made.append(node)
|
|
1162
|
+
self._search_position = node.child
|
|
1163
|
+
return
|
|
1059
1164
|
for description, checker in self._deferred_assumptions:
|
|
1060
1165
|
with ResumedTracing():
|
|
1061
1166
|
check_ret = checker()
|
|
1062
1167
|
if not prefer_true(check_ret):
|
|
1168
|
+
node = self.grow_into(DetachedPathNode())
|
|
1169
|
+
self.choices_made.append(node)
|
|
1170
|
+
self._search_position = node.child
|
|
1063
1171
|
raise IgnoreAttempt("deferred assumption failed: " + description)
|
|
1064
|
-
self.
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1172
|
+
if not isinstance(self._search_position, NodeStem):
|
|
1173
|
+
# Nondeterminism detected
|
|
1174
|
+
# We'll just overwrite the prior path with this one.
|
|
1175
|
+
# (note that this might leave some stats in the parents
|
|
1176
|
+
# that is no longer justified by the leaves?)
|
|
1177
|
+
if isinstance(self._search_position, DetachedPathNode):
|
|
1178
|
+
return
|
|
1179
|
+
previous_node = self._search_position
|
|
1180
|
+
node = DetachedPathNode()
|
|
1181
|
+
if self.choices_made:
|
|
1182
|
+
self.choices_made[-1].replace_child(previous_node, node)
|
|
1183
|
+
self.choices_made.append(node)
|
|
1184
|
+
self._search_position = node.child
|
|
1185
|
+
self.raise_not_deterministic(
|
|
1186
|
+
previous_node,
|
|
1187
|
+
f"Expect to detach path at a stem node, not at this node: {previous_node}",
|
|
1188
|
+
currently_handling=currently_handling,
|
|
1189
|
+
)
|
|
1190
|
+
node = self.grow_into(DetachedPathNode())
|
|
1068
1191
|
self.choices_made.append(node)
|
|
1069
1192
|
self._search_position = node.child
|
|
1070
1193
|
debug("Detached from search tree")
|
|
1071
1194
|
|
|
1072
1195
|
def cap_result_at_unknown(self):
|
|
1196
|
+
# TODO: this doesn't seem to work as intended.
|
|
1197
|
+
# If any execution path is confirmed, the end result is sometimes confirmed as well.
|
|
1073
1198
|
self.status_cap = VerificationStatus.UNKNOWN
|
|
1074
1199
|
|
|
1075
1200
|
def bubble_status(
|
|
1076
1201
|
self, analysis: CallAnalysis
|
|
1077
1202
|
) -> Tuple[Optional[CallAnalysis], bool]:
|
|
1078
1203
|
# In some cases, we might ignore an attempt while not at a leaf.
|
|
1079
|
-
if self._search_position
|
|
1080
|
-
self._search_position = self.
|
|
1081
|
-
SearchLeaf(analysis)
|
|
1082
|
-
)
|
|
1204
|
+
if isinstance(self._search_position, NodeStem):
|
|
1205
|
+
self._search_position = self.grow_into(SearchLeaf(analysis))
|
|
1083
1206
|
else:
|
|
1084
|
-
self._search_position = self._search_position.simplify()
|
|
1085
1207
|
assert isinstance(self._search_position, SearchTreeNode)
|
|
1086
1208
|
self._search_position.exhausted = True
|
|
1087
1209
|
self._search_position.result = analysis
|
|
@@ -1090,11 +1212,12 @@ class StateSpace:
|
|
|
1090
1212
|
return (analysis, True)
|
|
1091
1213
|
for node in reversed(self.choices_made):
|
|
1092
1214
|
node.update_result(analysis)
|
|
1093
|
-
if
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1215
|
+
if False: # this is more noise than it's worth (usually)
|
|
1216
|
+
if in_debug():
|
|
1217
|
+
for line in debug_path_tree(
|
|
1218
|
+
self._root, set(self.choices_made + [self._search_position])
|
|
1219
|
+
):
|
|
1220
|
+
debug(line)
|
|
1098
1221
|
# debug('Path summary:', self.choices_made)
|
|
1099
1222
|
first = self.choices_made[0]
|
|
1100
1223
|
analysis = first.get_result()
|