crosshair-tool 0.0.99__cp312-cp312-macosx_10_13_x86_64.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-312-darwin.so +0 -0
- crosshair/__init__.py +42 -0
- crosshair/__main__.py +8 -0
- crosshair/_mark_stacks.h +790 -0
- crosshair/_preliminaries_test.py +18 -0
- crosshair/_tracers.h +94 -0
- crosshair/_tracers_pycompat.h +522 -0
- crosshair/_tracers_test.py +138 -0
- crosshair/abcstring.py +245 -0
- crosshair/auditwall.py +190 -0
- crosshair/auditwall_test.py +77 -0
- crosshair/codeconfig.py +113 -0
- crosshair/codeconfig_test.py +117 -0
- crosshair/condition_parser.py +1237 -0
- crosshair/condition_parser_test.py +497 -0
- crosshair/conftest.py +30 -0
- crosshair/copyext.py +155 -0
- crosshair/copyext_test.py +84 -0
- crosshair/core.py +1763 -0
- crosshair/core_and_libs.py +149 -0
- crosshair/core_regestered_types_test.py +82 -0
- crosshair/core_test.py +1316 -0
- crosshair/diff_behavior.py +314 -0
- crosshair/diff_behavior_test.py +261 -0
- crosshair/dynamic_typing.py +346 -0
- crosshair/dynamic_typing_test.py +210 -0
- crosshair/enforce.py +282 -0
- crosshair/enforce_test.py +182 -0
- crosshair/examples/PEP316/__init__.py +1 -0
- crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
- crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
- crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
- crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
- crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
- crosshair/examples/PEP316/correct_code/__init__.py +0 -0
- crosshair/examples/PEP316/correct_code/arith.py +60 -0
- crosshair/examples/PEP316/correct_code/chess.py +77 -0
- crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
- crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
- crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
- crosshair/examples/PEP316/correct_code/showcase.py +104 -0
- crosshair/examples/__init__.py +0 -0
- crosshair/examples/check_examples_test.py +146 -0
- crosshair/examples/deal/__init__.py +1 -0
- crosshair/examples/icontract/__init__.py +1 -0
- crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
- crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
- crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
- crosshair/examples/icontract/correct_code/__init__.py +0 -0
- crosshair/examples/icontract/correct_code/arith.py +51 -0
- crosshair/examples/icontract/correct_code/showcase.py +94 -0
- crosshair/fnutil.py +391 -0
- crosshair/fnutil_test.py +75 -0
- crosshair/fuzz_core_test.py +516 -0
- crosshair/libimpl/__init__.py +0 -0
- crosshair/libimpl/arraylib.py +161 -0
- 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 +23 -0
- crosshair/libimpl/builtinslib.py +5228 -0
- crosshair/libimpl/builtinslib_ch_test.py +1191 -0
- crosshair/libimpl/builtinslib_test.py +3735 -0
- crosshair/libimpl/codecslib.py +86 -0
- crosshair/libimpl/codecslib_test.py +86 -0
- crosshair/libimpl/collectionslib.py +264 -0
- crosshair/libimpl/collectionslib_ch_test.py +252 -0
- crosshair/libimpl/collectionslib_test.py +332 -0
- crosshair/libimpl/copylib.py +23 -0
- crosshair/libimpl/copylib_test.py +18 -0
- crosshair/libimpl/datetimelib.py +2559 -0
- crosshair/libimpl/datetimelib_ch_test.py +354 -0
- crosshair/libimpl/datetimelib_test.py +112 -0
- crosshair/libimpl/decimallib.py +5257 -0
- crosshair/libimpl/decimallib_ch_test.py +78 -0
- crosshair/libimpl/decimallib_test.py +76 -0
- crosshair/libimpl/encodings/__init__.py +23 -0
- crosshair/libimpl/encodings/_encutil.py +187 -0
- crosshair/libimpl/encodings/ascii.py +44 -0
- crosshair/libimpl/encodings/latin_1.py +40 -0
- crosshair/libimpl/encodings/utf_8.py +93 -0
- crosshair/libimpl/encodings_ch_test.py +83 -0
- crosshair/libimpl/fractionlib.py +16 -0
- crosshair/libimpl/fractionlib_test.py +80 -0
- crosshair/libimpl/functoolslib.py +34 -0
- crosshair/libimpl/functoolslib_test.py +56 -0
- crosshair/libimpl/hashliblib.py +30 -0
- crosshair/libimpl/hashliblib_test.py +18 -0
- crosshair/libimpl/heapqlib.py +47 -0
- crosshair/libimpl/heapqlib_test.py +21 -0
- crosshair/libimpl/importliblib.py +18 -0
- crosshair/libimpl/importliblib_test.py +38 -0
- crosshair/libimpl/iolib.py +216 -0
- crosshair/libimpl/iolib_ch_test.py +128 -0
- crosshair/libimpl/iolib_test.py +19 -0
- crosshair/libimpl/ipaddresslib.py +8 -0
- crosshair/libimpl/itertoolslib.py +44 -0
- crosshair/libimpl/itertoolslib_test.py +44 -0
- crosshair/libimpl/jsonlib.py +984 -0
- crosshair/libimpl/jsonlib_ch_test.py +42 -0
- crosshair/libimpl/jsonlib_test.py +51 -0
- crosshair/libimpl/mathlib.py +179 -0
- crosshair/libimpl/mathlib_ch_test.py +44 -0
- crosshair/libimpl/mathlib_test.py +67 -0
- crosshair/libimpl/oslib.py +7 -0
- crosshair/libimpl/pathliblib_test.py +10 -0
- crosshair/libimpl/randomlib.py +178 -0
- crosshair/libimpl/randomlib_test.py +120 -0
- crosshair/libimpl/relib.py +846 -0
- crosshair/libimpl/relib_ch_test.py +169 -0
- crosshair/libimpl/relib_test.py +493 -0
- crosshair/libimpl/timelib.py +72 -0
- crosshair/libimpl/timelib_test.py +82 -0
- crosshair/libimpl/typeslib.py +15 -0
- crosshair/libimpl/typeslib_test.py +36 -0
- crosshair/libimpl/unicodedatalib.py +75 -0
- crosshair/libimpl/unicodedatalib_test.py +42 -0
- crosshair/libimpl/urlliblib.py +23 -0
- crosshair/libimpl/urlliblib_test.py +19 -0
- 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 +261 -0
- crosshair/lsp_server_test.py +30 -0
- crosshair/main.py +973 -0
- crosshair/main_test.py +543 -0
- crosshair/objectproxy.py +376 -0
- crosshair/objectproxy_test.py +41 -0
- crosshair/opcode_intercept.py +601 -0
- crosshair/opcode_intercept_test.py +304 -0
- crosshair/options.py +218 -0
- crosshair/options_test.py +10 -0
- crosshair/patch_equivalence_test.py +75 -0
- crosshair/path_cover.py +209 -0
- crosshair/path_cover_test.py +138 -0
- crosshair/path_search.py +161 -0
- crosshair/path_search_test.py +52 -0
- crosshair/pathing_oracle.py +271 -0
- crosshair/pathing_oracle_test.py +21 -0
- crosshair/pure_importer.py +27 -0
- crosshair/pure_importer_test.py +16 -0
- crosshair/py.typed +0 -0
- crosshair/register_contract.py +273 -0
- crosshair/register_contract_test.py +190 -0
- crosshair/simplestructs.py +1165 -0
- crosshair/simplestructs_test.py +283 -0
- crosshair/smtlib.py +24 -0
- crosshair/smtlib_test.py +14 -0
- crosshair/statespace.py +1199 -0
- crosshair/statespace_test.py +108 -0
- crosshair/stubs_parser.py +352 -0
- crosshair/stubs_parser_test.py +43 -0
- crosshair/test_util.py +329 -0
- crosshair/test_util_test.py +26 -0
- crosshair/tools/__init__.py +0 -0
- crosshair/tools/check_help_in_doc.py +264 -0
- crosshair/tools/check_init_and_setup_coincide.py +119 -0
- crosshair/tools/generate_demo_table.py +127 -0
- crosshair/tracers.py +544 -0
- crosshair/tracers_test.py +154 -0
- crosshair/type_repo.py +151 -0
- crosshair/unicode_categories.py +589 -0
- crosshair/unicode_categories_test.py +27 -0
- crosshair/util.py +741 -0
- crosshair/util_test.py +173 -0
- crosshair/watcher.py +307 -0
- crosshair/watcher_test.py +107 -0
- crosshair/z3util.py +76 -0
- crosshair/z3util_test.py +11 -0
- crosshair_tool-0.0.99.dist-info/METADATA +144 -0
- crosshair_tool-0.0.99.dist-info/RECORD +176 -0
- crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
- crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
- crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
- crosshair_tool-0.0.99.dist-info/top_level.txt +2 -0
crosshair/statespace.py
ADDED
|
@@ -0,0 +1,1199 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import builtins
|
|
3
|
+
import copy
|
|
4
|
+
import enum
|
|
5
|
+
import functools
|
|
6
|
+
import random
|
|
7
|
+
import re
|
|
8
|
+
import threading
|
|
9
|
+
from collections import Counter, defaultdict
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from sys import _getframe
|
|
12
|
+
from time import monotonic
|
|
13
|
+
from traceback import extract_stack, format_tb
|
|
14
|
+
from types import FrameType
|
|
15
|
+
from typing import (
|
|
16
|
+
Any,
|
|
17
|
+
Callable,
|
|
18
|
+
Dict,
|
|
19
|
+
List,
|
|
20
|
+
NewType,
|
|
21
|
+
NoReturn,
|
|
22
|
+
Optional,
|
|
23
|
+
Sequence,
|
|
24
|
+
Set,
|
|
25
|
+
Tuple,
|
|
26
|
+
Type,
|
|
27
|
+
TypeVar,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
import z3 # type: ignore
|
|
31
|
+
|
|
32
|
+
from crosshair import dynamic_typing
|
|
33
|
+
from crosshair.condition_parser import ConditionExpr
|
|
34
|
+
from crosshair.smtlib import parse_smtlib_literal
|
|
35
|
+
from crosshair.tracers import NoTracing, ResumedTracing, is_tracing
|
|
36
|
+
from crosshair.util import (
|
|
37
|
+
CROSSHAIR_EXTRA_ASSERTS,
|
|
38
|
+
CrossHairInternal,
|
|
39
|
+
IgnoreAttempt,
|
|
40
|
+
NotDeterministic,
|
|
41
|
+
PathTimeout,
|
|
42
|
+
UnknownSatisfiability,
|
|
43
|
+
assert_tracing,
|
|
44
|
+
ch_stack,
|
|
45
|
+
debug,
|
|
46
|
+
in_debug,
|
|
47
|
+
name_of_type,
|
|
48
|
+
)
|
|
49
|
+
from crosshair.z3util import z3Aassert, z3Not, z3Or, z3PopNot
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@functools.total_ordering
|
|
53
|
+
class MessageType(enum.Enum):
|
|
54
|
+
CONFIRMED = "confirmed"
|
|
55
|
+
# The postcondition returns True over all execution paths.
|
|
56
|
+
|
|
57
|
+
CANNOT_CONFIRM = "cannot_confirm"
|
|
58
|
+
# The postcondition returns True over the execution paths that were
|
|
59
|
+
# attempted.
|
|
60
|
+
|
|
61
|
+
PRE_UNSAT = "pre_unsat"
|
|
62
|
+
# No attempted execution path got past the precondition checks.
|
|
63
|
+
|
|
64
|
+
POST_ERR = "post_err"
|
|
65
|
+
# The postcondition raised an exception for some input.
|
|
66
|
+
|
|
67
|
+
EXEC_ERR = "exec_err"
|
|
68
|
+
# The body of the function raised an exception for some input.
|
|
69
|
+
|
|
70
|
+
POST_FAIL = "post_fail"
|
|
71
|
+
# The postcondition returned False for some input.
|
|
72
|
+
|
|
73
|
+
SYNTAX_ERR = "syntax_err"
|
|
74
|
+
# Pre/post conditions could not be determined.
|
|
75
|
+
|
|
76
|
+
IMPORT_ERR = "import_err"
|
|
77
|
+
# The requested module could not be imported.
|
|
78
|
+
|
|
79
|
+
def __repr__(self):
|
|
80
|
+
return f"MessageType.{self.name}"
|
|
81
|
+
|
|
82
|
+
def __lt__(self, other):
|
|
83
|
+
return self._order[self] < self._order[other]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
MessageType._order = { # type: ignore
|
|
87
|
+
# This is the order that messages override each other (for the same source
|
|
88
|
+
# file line).
|
|
89
|
+
# For exmaple, we prefer to report a False-returning postcondition
|
|
90
|
+
# (POST_FAIL) over an exception-raising postcondition (POST_ERR).
|
|
91
|
+
MessageType.CONFIRMED: 0,
|
|
92
|
+
MessageType.CANNOT_CONFIRM: 1,
|
|
93
|
+
MessageType.PRE_UNSAT: 2,
|
|
94
|
+
MessageType.POST_ERR: 3,
|
|
95
|
+
MessageType.EXEC_ERR: 4,
|
|
96
|
+
MessageType.POST_FAIL: 5,
|
|
97
|
+
MessageType.SYNTAX_ERR: 6,
|
|
98
|
+
MessageType.IMPORT_ERR: 7,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
CONFIRMED = MessageType.CONFIRMED
|
|
103
|
+
CANNOT_CONFIRM = MessageType.CANNOT_CONFIRM
|
|
104
|
+
PRE_UNSAT = MessageType.PRE_UNSAT
|
|
105
|
+
POST_ERR = MessageType.POST_ERR
|
|
106
|
+
EXEC_ERR = MessageType.EXEC_ERR
|
|
107
|
+
POST_FAIL = MessageType.POST_FAIL
|
|
108
|
+
SYNTAX_ERR = MessageType.SYNTAX_ERR
|
|
109
|
+
IMPORT_ERR = MessageType.IMPORT_ERR
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True)
|
|
113
|
+
class AnalysisMessage:
|
|
114
|
+
state: MessageType
|
|
115
|
+
message: str
|
|
116
|
+
filename: str
|
|
117
|
+
line: int
|
|
118
|
+
column: int
|
|
119
|
+
traceback: str
|
|
120
|
+
test_fn: Optional[str] = None
|
|
121
|
+
condition_src: Optional[str] = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@functools.total_ordering
|
|
125
|
+
class VerificationStatus(enum.Enum):
|
|
126
|
+
REFUTED = 0
|
|
127
|
+
UNKNOWN = 1
|
|
128
|
+
CONFIRMED = 2
|
|
129
|
+
|
|
130
|
+
def __repr__(self):
|
|
131
|
+
return f"VerificationStatus.{self.name}"
|
|
132
|
+
|
|
133
|
+
def __str__(self):
|
|
134
|
+
return self.name
|
|
135
|
+
|
|
136
|
+
def __lt__(self, other):
|
|
137
|
+
if self.__class__ is other.__class__:
|
|
138
|
+
return self.value < other.value
|
|
139
|
+
return NotImplemented
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class CallAnalysis:
|
|
144
|
+
verification_status: Optional[VerificationStatus] = None # None means "ignore"
|
|
145
|
+
messages: Sequence[AnalysisMessage] = ()
|
|
146
|
+
failing_precondition: Optional[ConditionExpr] = None
|
|
147
|
+
failing_precondition_reason: str = ""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
HeapRef = z3.DeclareSort("HeapRef")
|
|
151
|
+
SnapshotRef = NewType("SnapshotRef", int)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def model_value_to_python(value: z3.ExprRef) -> object:
|
|
155
|
+
if z3.is_real(value):
|
|
156
|
+
if isinstance(value, z3.AlgebraicNumRef):
|
|
157
|
+
# Force irrational values to be rational:
|
|
158
|
+
value = value.approx(precision=20)
|
|
159
|
+
return float(value.as_fraction())
|
|
160
|
+
elif z3.is_seq(value):
|
|
161
|
+
ret = []
|
|
162
|
+
while value.num_args() == 2:
|
|
163
|
+
ret.append(model_value_to_python(value.arg(0).arg(0)))
|
|
164
|
+
value = value.arg(1)
|
|
165
|
+
if value.num_args() == 1:
|
|
166
|
+
ret.append(model_value_to_python(value.arg(0)))
|
|
167
|
+
return ret
|
|
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
|
|
174
|
+
return value.as_long()
|
|
175
|
+
else:
|
|
176
|
+
return ast.literal_eval(repr(value))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@assert_tracing(False)
|
|
180
|
+
def prefer_true(v: Any) -> bool:
|
|
181
|
+
if not (hasattr(v, "var") and z3.is_bool(v.var)):
|
|
182
|
+
with ResumedTracing():
|
|
183
|
+
v = v.__bool__()
|
|
184
|
+
if not (hasattr(v, "var")):
|
|
185
|
+
return v
|
|
186
|
+
space = context_statespace()
|
|
187
|
+
return space.choose_possible(v.var, probability_true=1.0)
|
|
188
|
+
|
|
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
|
+
|
|
206
|
+
class StateSpaceCounter(Counter):
|
|
207
|
+
@property
|
|
208
|
+
def iterations(self) -> int:
|
|
209
|
+
return sum(self[s] for s in VerificationStatus) + self[None]
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def unknown_pct(self) -> float:
|
|
213
|
+
return self[VerificationStatus.UNKNOWN] / (self.iterations + 1)
|
|
214
|
+
|
|
215
|
+
def __str__(self) -> str:
|
|
216
|
+
parts = []
|
|
217
|
+
for k, ct in self.items():
|
|
218
|
+
if isinstance(k, enum.Enum):
|
|
219
|
+
k = k.name
|
|
220
|
+
parts.append(f"{k}:{ct}")
|
|
221
|
+
return "{" + ", ".join(parts) + "}"
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class AbstractPathingOracle:
|
|
225
|
+
def pre_path_hook(self, space: "StateSpace") -> None:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
def post_path_hook(self, path: Sequence["SearchTreeNode"]) -> None:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
def decide(
|
|
232
|
+
self, root, node: "WorstResultNode", engine_probability: Optional[float]
|
|
233
|
+
) -> float:
|
|
234
|
+
raise NotImplementedError
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# NOTE: CrossHair's monkey-patched getattr calls this function, so we
|
|
238
|
+
# force ourselves to use the builtin getattr, avoiding an infinite loop.
|
|
239
|
+
real_getattr = builtins.getattr
|
|
240
|
+
|
|
241
|
+
_THREAD_LOCALS = threading.local()
|
|
242
|
+
_THREAD_LOCALS.space = None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class StateSpaceContext:
|
|
246
|
+
def __init__(self, space: "StateSpace"):
|
|
247
|
+
self.space = space
|
|
248
|
+
|
|
249
|
+
def __enter__(self):
|
|
250
|
+
prev = real_getattr(_THREAD_LOCALS, "space", None)
|
|
251
|
+
if prev is not None:
|
|
252
|
+
raise CrossHairInternal("Already in a state space context")
|
|
253
|
+
space = self.space
|
|
254
|
+
_THREAD_LOCALS.space = space
|
|
255
|
+
space.mark_all_parent_frames()
|
|
256
|
+
|
|
257
|
+
def __exit__(self, exc_type, exc_value, tb):
|
|
258
|
+
prev = real_getattr(_THREAD_LOCALS, "space", None)
|
|
259
|
+
if prev is not self.space:
|
|
260
|
+
raise CrossHairInternal("State space was altered in context")
|
|
261
|
+
_THREAD_LOCALS.space = None
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def optional_context_statespace() -> Optional["StateSpace"]:
|
|
266
|
+
return _THREAD_LOCALS.space
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def context_statespace() -> "StateSpace":
|
|
270
|
+
space = _THREAD_LOCALS.space
|
|
271
|
+
if space is None:
|
|
272
|
+
raise CrossHairInternal("Not in a statespace context")
|
|
273
|
+
return space
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def newrandom():
|
|
277
|
+
return random.Random(1801243388510242075)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
_N = TypeVar("_N", bound="SearchTreeNode")
|
|
281
|
+
_T = TypeVar("_T")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class NodeLike:
|
|
285
|
+
def is_exhausted(self) -> bool:
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
def get_result(self) -> CallAnalysis:
|
|
289
|
+
"""
|
|
290
|
+
Get the result from the call.
|
|
291
|
+
|
|
292
|
+
post: implies(_.verification_status == VerificationStatus.CONFIRMED, self.is_exhausted())
|
|
293
|
+
"""
|
|
294
|
+
raise NotImplementedError
|
|
295
|
+
|
|
296
|
+
def stats(self) -> StateSpaceCounter:
|
|
297
|
+
raise NotImplementedError
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class SearchTreeNode(NodeLike):
|
|
301
|
+
"""A node in the execution path tree."""
|
|
302
|
+
|
|
303
|
+
stacktail: Tuple[str, ...] = ()
|
|
304
|
+
result: CallAnalysis = CallAnalysis()
|
|
305
|
+
exhausted: bool = False
|
|
306
|
+
iteration: Optional[int] = None
|
|
307
|
+
|
|
308
|
+
def choose(
|
|
309
|
+
self, space: "StateSpace", probability_true: Optional[float] = None
|
|
310
|
+
) -> Tuple[bool, float, NodeLike]:
|
|
311
|
+
raise NotImplementedError
|
|
312
|
+
|
|
313
|
+
def is_exhausted(self) -> bool:
|
|
314
|
+
return self.exhausted
|
|
315
|
+
|
|
316
|
+
def get_result(self) -> CallAnalysis:
|
|
317
|
+
return self.result
|
|
318
|
+
|
|
319
|
+
def update_result(self, leaf_analysis: CallAnalysis) -> bool:
|
|
320
|
+
if not self.exhausted:
|
|
321
|
+
next_result, next_exhausted = self.compute_result(leaf_analysis)
|
|
322
|
+
if next_exhausted != self.exhausted or next_result != self.result:
|
|
323
|
+
self.result, self.exhausted = next_result, next_exhausted
|
|
324
|
+
return True
|
|
325
|
+
return False
|
|
326
|
+
|
|
327
|
+
def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
|
|
328
|
+
raise NotImplementedError
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class NodeStem(NodeLike):
|
|
332
|
+
def __init__(self, parent: SearchTreeNode, parent_attr_name: str):
|
|
333
|
+
self.parent = parent
|
|
334
|
+
self.parent_attr_name = parent_attr_name
|
|
335
|
+
|
|
336
|
+
def grow(self, node: SearchTreeNode):
|
|
337
|
+
setattr(self.parent, self.parent_attr_name, node)
|
|
338
|
+
|
|
339
|
+
def is_exhausted(self) -> bool:
|
|
340
|
+
return False
|
|
341
|
+
|
|
342
|
+
def get_result(self) -> CallAnalysis:
|
|
343
|
+
return CallAnalysis(VerificationStatus.UNKNOWN)
|
|
344
|
+
|
|
345
|
+
def stats(self) -> StateSpaceCounter:
|
|
346
|
+
return StateSpaceCounter()
|
|
347
|
+
|
|
348
|
+
def __repr__(self) -> str:
|
|
349
|
+
return "NodeStem()"
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def solver_is_sat(solver, *exprs) -> bool:
|
|
353
|
+
ret = solver.check(*exprs)
|
|
354
|
+
if ret == z3.unknown:
|
|
355
|
+
debug("Z3 Unknown satisfiability. Reason:", solver.reason_unknown())
|
|
356
|
+
debug("Call stack at time of unknown sat:", ch_stack())
|
|
357
|
+
if solver.reason_unknown() == "interrupted from keyboard":
|
|
358
|
+
raise KeyboardInterrupt
|
|
359
|
+
if exprs:
|
|
360
|
+
debug("While attempting to assert\n", *(e.sexpr() for e in exprs))
|
|
361
|
+
debug("Solver state follows:\n", solver.sexpr())
|
|
362
|
+
raise UnknownSatisfiability
|
|
363
|
+
return ret == z3.sat
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def node_result(node: Optional[NodeLike]) -> Optional[CallAnalysis]:
|
|
367
|
+
if node is None:
|
|
368
|
+
return None
|
|
369
|
+
return node.get_result()
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def node_status(node: Optional[NodeLike]) -> Optional[VerificationStatus]:
|
|
373
|
+
result = node_result(node)
|
|
374
|
+
if result is not None:
|
|
375
|
+
return result.verification_status
|
|
376
|
+
else:
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class SearchLeaf(SearchTreeNode):
|
|
381
|
+
def __init__(self, result: CallAnalysis):
|
|
382
|
+
self.result = result
|
|
383
|
+
self.exhausted = True
|
|
384
|
+
self._stats = StateSpaceCounter({result.verification_status: 1})
|
|
385
|
+
|
|
386
|
+
def stats(self) -> StateSpaceCounter:
|
|
387
|
+
return self._stats
|
|
388
|
+
|
|
389
|
+
def __str__(self) -> str:
|
|
390
|
+
return f"{self.__class__.__name__}({self.result.verification_status})"
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class SinglePathNode(SearchTreeNode):
|
|
394
|
+
decision: bool
|
|
395
|
+
child: NodeLike
|
|
396
|
+
_random: random.Random
|
|
397
|
+
|
|
398
|
+
def __init__(self, decision: bool):
|
|
399
|
+
self.decision = decision
|
|
400
|
+
self.child = NodeStem(self, "child")
|
|
401
|
+
self._random = newrandom()
|
|
402
|
+
|
|
403
|
+
def choose(
|
|
404
|
+
self, space: "StateSpace", probability_true: Optional[float] = None
|
|
405
|
+
) -> Tuple[bool, float, NodeLike]:
|
|
406
|
+
return (self.decision, 1.0, self.child)
|
|
407
|
+
|
|
408
|
+
def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
|
|
409
|
+
assert isinstance(self.child, SearchTreeNode)
|
|
410
|
+
return (self.child.get_result(), self.child.is_exhausted())
|
|
411
|
+
|
|
412
|
+
def stats(self) -> StateSpaceCounter:
|
|
413
|
+
return self.child.stats()
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class BranchCounter:
|
|
417
|
+
__slots__ = ["pos_ct", "neg_ct"]
|
|
418
|
+
pos_ct: int
|
|
419
|
+
neg_ct: int
|
|
420
|
+
|
|
421
|
+
def __init__(self):
|
|
422
|
+
self.pos_ct = 0
|
|
423
|
+
self.neg_ct = 0
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class RootNode(SinglePathNode):
|
|
427
|
+
def __init__(self):
|
|
428
|
+
super().__init__(True)
|
|
429
|
+
self._open_coverage: Dict[Tuple[str, ...], BranchCounter] = defaultdict(
|
|
430
|
+
BranchCounter
|
|
431
|
+
)
|
|
432
|
+
from crosshair.pathing_oracle import CoveragePathingOracle # circular import
|
|
433
|
+
|
|
434
|
+
self.pathing_oracle: AbstractPathingOracle = CoveragePathingOracle()
|
|
435
|
+
self.iteration = 0
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class DetachedPathNode(SinglePathNode):
|
|
439
|
+
def __init__(self):
|
|
440
|
+
super().__init__(True)
|
|
441
|
+
# Seems like `exhausted` should be True, but we set to False until we can
|
|
442
|
+
# collect the result from path's leaf. (exhaustion prevents caches from
|
|
443
|
+
# updating)
|
|
444
|
+
self.exhausted = False
|
|
445
|
+
self._stats = None
|
|
446
|
+
|
|
447
|
+
def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
|
|
448
|
+
return (leaf_analysis, True)
|
|
449
|
+
|
|
450
|
+
def stats(self) -> StateSpaceCounter:
|
|
451
|
+
if self._stats is None:
|
|
452
|
+
self._stats = StateSpaceCounter(
|
|
453
|
+
{
|
|
454
|
+
k: v
|
|
455
|
+
for k, v in self.child.stats().items()
|
|
456
|
+
# We only propagate the verification status.
|
|
457
|
+
# (we should mostly look like a SearchLeaf)
|
|
458
|
+
if isinstance(k, VerificationStatus)
|
|
459
|
+
}
|
|
460
|
+
)
|
|
461
|
+
return self._stats
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class BinaryPathNode(SearchTreeNode):
|
|
465
|
+
positive: NodeLike
|
|
466
|
+
negative: NodeLike
|
|
467
|
+
|
|
468
|
+
def __init__(self):
|
|
469
|
+
self._stats = StateSpaceCounter()
|
|
470
|
+
|
|
471
|
+
def stats_lookahead(self) -> Tuple[StateSpaceCounter, StateSpaceCounter]:
|
|
472
|
+
return (self.positive.stats(), self.negative.stats())
|
|
473
|
+
|
|
474
|
+
def stats(self) -> StateSpaceCounter:
|
|
475
|
+
return self._stats
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class RandomizedBinaryPathNode(BinaryPathNode):
|
|
479
|
+
def __init__(self, rand: random.Random):
|
|
480
|
+
super().__init__()
|
|
481
|
+
self._random = rand
|
|
482
|
+
self.positive = NodeStem(self, "positive")
|
|
483
|
+
self.negative = NodeStem(self, "negative")
|
|
484
|
+
|
|
485
|
+
def probability_true(
|
|
486
|
+
self, space: "StateSpace", requested_probability: Optional[float] = None
|
|
487
|
+
) -> float:
|
|
488
|
+
raise NotImplementedError
|
|
489
|
+
|
|
490
|
+
def choose(
|
|
491
|
+
self, space: "StateSpace", probability_true: Optional[float] = None
|
|
492
|
+
) -> Tuple[bool, float, NodeLike]:
|
|
493
|
+
positive_ok = not self.positive.is_exhausted()
|
|
494
|
+
negative_ok = not self.negative.is_exhausted()
|
|
495
|
+
assert positive_ok or negative_ok
|
|
496
|
+
if positive_ok and negative_ok:
|
|
497
|
+
probability_true = self.probability_true(
|
|
498
|
+
space, requested_probability=probability_true
|
|
499
|
+
)
|
|
500
|
+
randval = self._random.uniform(0.000_001, 0.999_999)
|
|
501
|
+
if randval < probability_true:
|
|
502
|
+
return (True, probability_true, self.positive)
|
|
503
|
+
else:
|
|
504
|
+
return (False, 1.0 - probability_true, self.negative)
|
|
505
|
+
else:
|
|
506
|
+
return (positive_ok, 1.0, self.positive if positive_ok else self.negative)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
class ParallelNode(RandomizedBinaryPathNode):
|
|
510
|
+
"""Choose either path; the first complete result will be used."""
|
|
511
|
+
|
|
512
|
+
def __init__(self, rand: random.Random, false_probability: float, desc: str):
|
|
513
|
+
super().__init__(rand)
|
|
514
|
+
self._false_probability = false_probability
|
|
515
|
+
self._desc = desc
|
|
516
|
+
|
|
517
|
+
def __repr__(self):
|
|
518
|
+
return f"ParallelNode(false_pct={self._false_probability}, {self._desc})"
|
|
519
|
+
|
|
520
|
+
def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
|
|
521
|
+
positive, negative = self.positive, self.negative
|
|
522
|
+
pos_exhausted = positive.is_exhausted()
|
|
523
|
+
neg_exhausted = negative.is_exhausted()
|
|
524
|
+
if pos_exhausted and not node_status(positive) == VerificationStatus.UNKNOWN:
|
|
525
|
+
self._stats = positive.stats()
|
|
526
|
+
return (positive.get_result(), True)
|
|
527
|
+
if neg_exhausted and not node_status(negative) == VerificationStatus.UNKNOWN:
|
|
528
|
+
self._stats = negative.stats()
|
|
529
|
+
return (negative.get_result(), True)
|
|
530
|
+
# it's unclear whether we want to just add stats here:
|
|
531
|
+
self._stats = StateSpaceCounter(positive.stats() + negative.stats())
|
|
532
|
+
return merge_node_results(
|
|
533
|
+
positive.get_result(),
|
|
534
|
+
pos_exhausted and neg_exhausted,
|
|
535
|
+
negative,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def probability_true(
|
|
539
|
+
self, space: "StateSpace", requested_probability: Optional[float] = None
|
|
540
|
+
) -> float:
|
|
541
|
+
if self.negative.is_exhausted():
|
|
542
|
+
return 1.0
|
|
543
|
+
elif requested_probability is not None:
|
|
544
|
+
return requested_probability
|
|
545
|
+
else:
|
|
546
|
+
return 1.0 - self._false_probability
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def merge_node_results(
|
|
550
|
+
left: CallAnalysis, exhausted: bool, node: NodeLike
|
|
551
|
+
) -> Tuple[CallAnalysis, bool]:
|
|
552
|
+
"""
|
|
553
|
+
Merge analysis from different branches of code.
|
|
554
|
+
|
|
555
|
+
Combines messages, take the worst verification status of the two, etc.
|
|
556
|
+
"""
|
|
557
|
+
right = node.get_result()
|
|
558
|
+
if not node.is_exhausted():
|
|
559
|
+
exhausted = False
|
|
560
|
+
if left.verification_status is None:
|
|
561
|
+
return (right, exhausted)
|
|
562
|
+
if right.verification_status is None:
|
|
563
|
+
return (left, exhausted)
|
|
564
|
+
if left.failing_precondition and right.failing_precondition:
|
|
565
|
+
lc, rc = left.failing_precondition, right.failing_precondition
|
|
566
|
+
precond_side = left if lc.line > rc.line else right
|
|
567
|
+
else:
|
|
568
|
+
precond_side = left if left.failing_precondition else right
|
|
569
|
+
return (
|
|
570
|
+
CallAnalysis(
|
|
571
|
+
min(left.verification_status, right.verification_status),
|
|
572
|
+
list(left.messages) + list(right.messages),
|
|
573
|
+
precond_side.failing_precondition,
|
|
574
|
+
precond_side.failing_precondition_reason,
|
|
575
|
+
),
|
|
576
|
+
exhausted,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
_RE_WHITESPACE_SUB = re.compile(r"\s+").sub
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
class WorstResultNode(RandomizedBinaryPathNode):
|
|
584
|
+
forced_path: Optional[bool] = None
|
|
585
|
+
expr: Optional[z3.ExprRef] = None
|
|
586
|
+
normalized_expr: Tuple[bool, z3.ExprRef]
|
|
587
|
+
|
|
588
|
+
def __init__(self, rand: random.Random, expr: z3.ExprRef, solver: z3.Solver):
|
|
589
|
+
super().__init__(rand)
|
|
590
|
+
is_positive, root_expr = z3PopNot(expr)
|
|
591
|
+
self.normalized_expr = (is_positive, root_expr)
|
|
592
|
+
notexpr = z3Not(expr) if is_positive else root_expr
|
|
593
|
+
if solver_is_sat(solver, notexpr):
|
|
594
|
+
if not solver_is_sat(solver, expr):
|
|
595
|
+
self.forced_path = False
|
|
596
|
+
else:
|
|
597
|
+
# We run into soundness issues on occasion:
|
|
598
|
+
if CROSSHAIR_EXTRA_ASSERTS and not solver_is_sat(solver, expr):
|
|
599
|
+
debug(" *** Reached impossible code path *** ")
|
|
600
|
+
debug("Current solver state:\n", str(solver))
|
|
601
|
+
raise CrossHairInternal("Reached impossible code path")
|
|
602
|
+
self.forced_path = True
|
|
603
|
+
self.expr = expr
|
|
604
|
+
|
|
605
|
+
def _is_exhausted(self):
|
|
606
|
+
return (
|
|
607
|
+
(self.positive.is_exhausted() and self.negative.is_exhausted())
|
|
608
|
+
or (self.forced_path is True and self.positive.is_exhausted())
|
|
609
|
+
or (self.forced_path is False and self.negative.is_exhausted())
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
def __repr__(self):
|
|
613
|
+
smt_expr = _RE_WHITESPACE_SUB(" ", str(self.expr))
|
|
614
|
+
exhausted = " : exhausted" if self._is_exhausted() else ""
|
|
615
|
+
forced = f" : force={self.forced_path}" if self.forced_path is not None else ""
|
|
616
|
+
return f"{self.__class__.__name__}({smt_expr}{exhausted}{forced})"
|
|
617
|
+
|
|
618
|
+
def choose(
|
|
619
|
+
self, space: "StateSpace", probability_true: Optional[float] = None
|
|
620
|
+
) -> Tuple[bool, float, NodeLike]:
|
|
621
|
+
if self.forced_path is None:
|
|
622
|
+
return RandomizedBinaryPathNode.choose(self, space, probability_true)
|
|
623
|
+
return (
|
|
624
|
+
self.forced_path,
|
|
625
|
+
1.0,
|
|
626
|
+
self.positive if self.forced_path else self.negative,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
def probability_true(
|
|
630
|
+
self, space: "StateSpace", requested_probability: Optional[float] = None
|
|
631
|
+
) -> float:
|
|
632
|
+
root = space._root
|
|
633
|
+
return root.pathing_oracle.decide(
|
|
634
|
+
root, self, engine_probability=requested_probability
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
|
|
638
|
+
positive, negative = self.positive, self.negative
|
|
639
|
+
exhausted = self._is_exhausted()
|
|
640
|
+
if node_status(positive) == VerificationStatus.REFUTED or (
|
|
641
|
+
self.forced_path is True
|
|
642
|
+
):
|
|
643
|
+
self._stats = positive.stats()
|
|
644
|
+
return (positive.get_result(), exhausted)
|
|
645
|
+
if node_status(negative) == VerificationStatus.REFUTED or (
|
|
646
|
+
self.forced_path is False
|
|
647
|
+
):
|
|
648
|
+
self._stats = negative.stats()
|
|
649
|
+
return (negative.get_result(), exhausted)
|
|
650
|
+
self._stats = StateSpaceCounter(positive.stats() + negative.stats())
|
|
651
|
+
return merge_node_results(
|
|
652
|
+
positive.get_result(), positive.is_exhausted(), negative
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
class ModelValueNode(WorstResultNode):
|
|
657
|
+
condition_value: object = None
|
|
658
|
+
|
|
659
|
+
def __init__(self, rand: random.Random, expr: z3.ExprRef, solver: z3.Solver):
|
|
660
|
+
if not solver_is_sat(solver):
|
|
661
|
+
debug("Solver unexpectedly unsat; solver state:", solver.sexpr())
|
|
662
|
+
raise CrossHairInternal("Unexpected unsat from solver")
|
|
663
|
+
|
|
664
|
+
self.condition_value = solver.model().evaluate(expr, model_completion=True)
|
|
665
|
+
self._stats_key = f"realize_{expr}" if z3.is_const(expr) else None
|
|
666
|
+
WorstResultNode.__init__(self, rand, expr == self.condition_value, solver)
|
|
667
|
+
|
|
668
|
+
def compute_result(self, leaf_analysis: CallAnalysis) -> Tuple[CallAnalysis, bool]:
|
|
669
|
+
stats_key = self._stats_key
|
|
670
|
+
old_realizations = self._stats[stats_key]
|
|
671
|
+
analysis, is_exhausted = super().compute_result(leaf_analysis)
|
|
672
|
+
if stats_key:
|
|
673
|
+
self._stats[stats_key] = old_realizations + 1
|
|
674
|
+
return (analysis, is_exhausted)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def debug_path_tree(node, highlights, prefix="") -> List[str]:
|
|
678
|
+
highlighted = node in highlights
|
|
679
|
+
highlighted |= node in highlights
|
|
680
|
+
if isinstance(node, BinaryPathNode):
|
|
681
|
+
if isinstance(node, WorstResultNode) and node.forced_path is not None:
|
|
682
|
+
return debug_path_tree(
|
|
683
|
+
node.positive if node.forced_path else node.negative, highlights, prefix
|
|
684
|
+
)
|
|
685
|
+
lines = []
|
|
686
|
+
forkstr = r"n|\ y " if isinstance(node, WorstResultNode) else r" |\ "
|
|
687
|
+
if highlighted:
|
|
688
|
+
lines.append(f"{prefix}{forkstr}*{str(node)} {node.stats()}")
|
|
689
|
+
else:
|
|
690
|
+
lines.append(f"{prefix}{forkstr}{str(node)} {node.stats()}")
|
|
691
|
+
if node.is_exhausted() and not highlighted:
|
|
692
|
+
return lines # collapse fully explored subtrees
|
|
693
|
+
lines.extend(debug_path_tree(node.positive, highlights, prefix + " | "))
|
|
694
|
+
lines.extend(debug_path_tree(node.negative, highlights, prefix))
|
|
695
|
+
return lines
|
|
696
|
+
elif isinstance(node, SinglePathNode):
|
|
697
|
+
lines = []
|
|
698
|
+
if highlighted:
|
|
699
|
+
lines.append(f"{prefix} | *{type(node).__name__} {node.stats()}")
|
|
700
|
+
else:
|
|
701
|
+
lines.append(f"{prefix} | {type(node).__name__} {node.stats()}")
|
|
702
|
+
lines.extend(debug_path_tree(node.child, highlights, prefix))
|
|
703
|
+
return lines
|
|
704
|
+
else:
|
|
705
|
+
if highlighted:
|
|
706
|
+
return [f"{prefix} -> *{str(node)} {node.stats()}"]
|
|
707
|
+
else:
|
|
708
|
+
return [f"{prefix} -> {str(node)} {node.stats()}"]
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def make_default_solver() -> z3.Solver:
|
|
712
|
+
"""Create a new solver with default settings."""
|
|
713
|
+
smt_tactic = z3.Tactic("smt")
|
|
714
|
+
solver = smt_tactic.solver()
|
|
715
|
+
solver.set("mbqi", True)
|
|
716
|
+
# turn off every randomization thing we can think of:
|
|
717
|
+
solver.set("random-seed", 42)
|
|
718
|
+
solver.set("smt.random-seed", 42)
|
|
719
|
+
return solver
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
class StateSpace:
|
|
723
|
+
"""Holds various information about the SMT solver's current state."""
|
|
724
|
+
|
|
725
|
+
_search_position: NodeLike
|
|
726
|
+
_deferred_assumptions: List[Tuple[str, Callable[[], bool]]]
|
|
727
|
+
_extras: Dict[Type, object]
|
|
728
|
+
|
|
729
|
+
def __init__(
|
|
730
|
+
self,
|
|
731
|
+
execution_deadline: float,
|
|
732
|
+
model_check_timeout: float,
|
|
733
|
+
search_root: RootNode,
|
|
734
|
+
):
|
|
735
|
+
self.solver = make_default_solver()
|
|
736
|
+
if model_check_timeout < 1 << 63:
|
|
737
|
+
self.smt_timeout: Optional[int] = int(model_check_timeout * 1000 + 1)
|
|
738
|
+
self.solver.set(timeout=self.smt_timeout)
|
|
739
|
+
else:
|
|
740
|
+
self.smt_timeout = None
|
|
741
|
+
self.choices_made: List[SearchTreeNode] = []
|
|
742
|
+
self.status_cap: Optional[VerificationStatus] = None
|
|
743
|
+
self.heaps: List[List[Tuple[z3.ExprRef, Type, object]]] = [[]]
|
|
744
|
+
self.next_uniq = 1
|
|
745
|
+
self.is_detached = False
|
|
746
|
+
self._extras = {}
|
|
747
|
+
self._already_logged: Set[z3.ExprRef] = set()
|
|
748
|
+
self._exprs_known: Dict[z3.ExprRef, bool] = {}
|
|
749
|
+
|
|
750
|
+
self.execution_deadline = execution_deadline
|
|
751
|
+
self._root = search_root
|
|
752
|
+
self._random = search_root._random
|
|
753
|
+
_, _, self._search_position = search_root.choose(self)
|
|
754
|
+
self._deferred_assumptions = []
|
|
755
|
+
assert search_root.iteration is not None
|
|
756
|
+
search_root.iteration += 1
|
|
757
|
+
search_root.pathing_oracle.pre_path_hook(self)
|
|
758
|
+
|
|
759
|
+
def add(self, expr) -> None:
|
|
760
|
+
with NoTracing():
|
|
761
|
+
if hasattr(expr, "var"):
|
|
762
|
+
expr = expr.var
|
|
763
|
+
elif not isinstance(expr, z3.ExprRef):
|
|
764
|
+
if type(expr) is bool:
|
|
765
|
+
raise CrossHairInternal(
|
|
766
|
+
"Attempted to assert a concrete boolean (look for unexpected realization)"
|
|
767
|
+
)
|
|
768
|
+
raise CrossHairInternal(
|
|
769
|
+
"Expected symbolic boolean, but supplied expression of type",
|
|
770
|
+
name_of_type(type(expr)),
|
|
771
|
+
)
|
|
772
|
+
# debug('Committed to ', expr)
|
|
773
|
+
already_known = self._exprs_known.get(expr)
|
|
774
|
+
if already_known is None:
|
|
775
|
+
self.solver.add(expr)
|
|
776
|
+
self._exprs_known[expr] = True
|
|
777
|
+
elif already_known is not True:
|
|
778
|
+
raise CrossHairInternal
|
|
779
|
+
|
|
780
|
+
def rand(self) -> random.Random:
|
|
781
|
+
return self._random
|
|
782
|
+
|
|
783
|
+
def extra(self, typ: Type[_T]) -> _T:
|
|
784
|
+
"""Get an object whose lifetime is tied to that of the SMT solver."""
|
|
785
|
+
value = self._extras.get(typ)
|
|
786
|
+
if value is None:
|
|
787
|
+
value = typ(self.solver) # type: ignore
|
|
788
|
+
self._extras[typ] = value
|
|
789
|
+
return value # type: ignore
|
|
790
|
+
|
|
791
|
+
def stats_lookahead(self) -> Tuple[StateSpaceCounter, StateSpaceCounter]:
|
|
792
|
+
node = self._search_position
|
|
793
|
+
if isinstance(node, NodeStem):
|
|
794
|
+
return (StateSpaceCounter(), StateSpaceCounter())
|
|
795
|
+
assert isinstance(node, BinaryPathNode), f"node {node} is not a binarypathnode"
|
|
796
|
+
return node.stats_lookahead()
|
|
797
|
+
|
|
798
|
+
def grow_into(self, node: _N) -> _N:
|
|
799
|
+
assert isinstance(self._search_position, NodeStem)
|
|
800
|
+
self._search_position.grow(node)
|
|
801
|
+
node.iteration = self._root.iteration
|
|
802
|
+
self._search_position = node
|
|
803
|
+
return node
|
|
804
|
+
|
|
805
|
+
def fork_parallel(self, false_probability: float, desc: str = "") -> bool:
|
|
806
|
+
node = self._search_position
|
|
807
|
+
if isinstance(node, NodeStem):
|
|
808
|
+
node = self.grow_into(ParallelNode(self._random, false_probability, desc))
|
|
809
|
+
node.stacktail = self.gen_stack_descriptions()
|
|
810
|
+
assert isinstance(node, ParallelNode)
|
|
811
|
+
self._search_position = node
|
|
812
|
+
else:
|
|
813
|
+
if not isinstance(node, ParallelNode):
|
|
814
|
+
self.raise_not_deterministic(
|
|
815
|
+
node, "Wrong node type (expected ParallelNode)"
|
|
816
|
+
)
|
|
817
|
+
node._false_probability = false_probability
|
|
818
|
+
self.choices_made.append(node)
|
|
819
|
+
ret, _, next_node = node.choose(self)
|
|
820
|
+
self._search_position = next_node
|
|
821
|
+
return ret
|
|
822
|
+
|
|
823
|
+
def is_possible(self, expr) -> bool:
|
|
824
|
+
with NoTracing():
|
|
825
|
+
if hasattr(expr, "var"):
|
|
826
|
+
expr = expr.var
|
|
827
|
+
debug("is possible?", expr)
|
|
828
|
+
return solver_is_sat(self.solver, expr)
|
|
829
|
+
|
|
830
|
+
def mark_all_parent_frames(self):
|
|
831
|
+
frames: Set[FrameType] = set()
|
|
832
|
+
frame = _getframe()
|
|
833
|
+
while frame and frame not in frames:
|
|
834
|
+
frames.add(frame)
|
|
835
|
+
frame = frame.f_back
|
|
836
|
+
self.external_frames = (
|
|
837
|
+
frames # just to prevent dealllocation and keep the id()s valid
|
|
838
|
+
)
|
|
839
|
+
self.external_frame_ids = {id(f) for f in frames}
|
|
840
|
+
|
|
841
|
+
def gen_stack_descriptions(self) -> Tuple[str, ...]:
|
|
842
|
+
f: Any = _getframe().f_back.f_back # type: ignore
|
|
843
|
+
frames = [f := f.f_back or f for _ in range(8)]
|
|
844
|
+
# TODO: To help oracles, I'd like to add sub-line resolution via f.f_lasti;
|
|
845
|
+
# however, in Python >= 3.11, the instruction pointer can shift between
|
|
846
|
+
# PRECALL and CALL opcodes, triggering our nondeterminism check.
|
|
847
|
+
return tuple(f"{f.f_code.co_filename}:{f.f_lineno}" for f in frames)
|
|
848
|
+
|
|
849
|
+
def check_timeout(self):
|
|
850
|
+
if monotonic() > self.execution_deadline:
|
|
851
|
+
debug(
|
|
852
|
+
"Path execution timeout after making ",
|
|
853
|
+
len(self.choices_made),
|
|
854
|
+
" choices.",
|
|
855
|
+
)
|
|
856
|
+
raise PathTimeout
|
|
857
|
+
|
|
858
|
+
@assert_tracing(False)
|
|
859
|
+
def choose_possible(
|
|
860
|
+
self, expr: z3.ExprRef, probability_true: Optional[float] = None
|
|
861
|
+
) -> bool:
|
|
862
|
+
known_result = self._exprs_known.get(expr)
|
|
863
|
+
if isinstance(known_result, bool):
|
|
864
|
+
return known_result
|
|
865
|
+
# NOTE: format_stack() is more human readable, but it pulls source file contents,
|
|
866
|
+
# so it is (1) slow, and (2) unstable when source code changes while we are checking.
|
|
867
|
+
stacktail = self.gen_stack_descriptions()
|
|
868
|
+
if isinstance(self._search_position, SearchTreeNode):
|
|
869
|
+
node = self._search_position
|
|
870
|
+
not_deterministic_reason = (
|
|
871
|
+
(
|
|
872
|
+
(not isinstance(node, WorstResultNode))
|
|
873
|
+
and f"Wrong node type (is {name_of_type(type(node))}, expected WorstResultNode)"
|
|
874
|
+
)
|
|
875
|
+
# TODO: Not clear whether we want this stack trace check.
|
|
876
|
+
# A stack change usually indicates a serious problem, but not 100% of the time.
|
|
877
|
+
# Keeping it would mean that we fail earlier.
|
|
878
|
+
# But also see https://github.com/HypothesisWorks/hypothesis/pull/4034#issuecomment-2606415404
|
|
879
|
+
# or (node.stacktail != stacktail and "Stack trace changed")
|
|
880
|
+
or (
|
|
881
|
+
(hasattr(node, "expr") and (not z3.eq(node.expr, expr)))
|
|
882
|
+
and "SMT expression changed"
|
|
883
|
+
)
|
|
884
|
+
)
|
|
885
|
+
if not_deterministic_reason:
|
|
886
|
+
self.raise_not_deterministic(
|
|
887
|
+
node, not_deterministic_reason, expr=expr, stacktail=stacktail
|
|
888
|
+
)
|
|
889
|
+
else:
|
|
890
|
+
# We only allow time outs at stems - that's because we don't want
|
|
891
|
+
# to think about how mutating an existing path branch would work:
|
|
892
|
+
self.check_timeout()
|
|
893
|
+
node = self.grow_into(WorstResultNode(self._random, expr, self.solver))
|
|
894
|
+
node.stacktail = stacktail
|
|
895
|
+
|
|
896
|
+
self._search_position = node
|
|
897
|
+
choose_true, chosen_probability, stem = node.choose(
|
|
898
|
+
self, probability_true=probability_true
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
branch_counter = self._root._open_coverage[stacktail]
|
|
902
|
+
if choose_true:
|
|
903
|
+
branch_counter.pos_ct += 1
|
|
904
|
+
else:
|
|
905
|
+
branch_counter.neg_ct += 1
|
|
906
|
+
|
|
907
|
+
self.choices_made.append(node)
|
|
908
|
+
self._search_position = stem
|
|
909
|
+
chosen_expr = expr if choose_true else z3Not(expr)
|
|
910
|
+
if in_debug():
|
|
911
|
+
debug(
|
|
912
|
+
f"SMT chose: {chosen_expr} (chance: {chosen_probability}) at",
|
|
913
|
+
ch_stack(),
|
|
914
|
+
)
|
|
915
|
+
z3Aassert(self.solver, chosen_expr)
|
|
916
|
+
self._exprs_known[expr] = choose_true
|
|
917
|
+
return choose_true
|
|
918
|
+
|
|
919
|
+
def raise_not_deterministic(
|
|
920
|
+
self,
|
|
921
|
+
node: NodeLike,
|
|
922
|
+
reason: str,
|
|
923
|
+
expr: Optional[z3.ExprRef] = None,
|
|
924
|
+
stacktail: Optional[Tuple[str, ...]] = None,
|
|
925
|
+
currently_handling: Optional[BaseException] = None,
|
|
926
|
+
) -> NoReturn:
|
|
927
|
+
lines = ["*** Begin Not Deterministic Debug ***"]
|
|
928
|
+
if getattr(node, "iteration", None) is not None:
|
|
929
|
+
lines.append(f"Previous iteration: {node.iteration}") # type: ignore
|
|
930
|
+
if hasattr(node, "expr"):
|
|
931
|
+
lines.append(f"Previous SMT expression: {node.expr}") # type: ignore
|
|
932
|
+
if expr is not None:
|
|
933
|
+
lines.append(f"Current SMT expression: {expr}")
|
|
934
|
+
if not stacktail:
|
|
935
|
+
if currently_handling is not None:
|
|
936
|
+
stacktail = tuple(format_tb(currently_handling.__traceback__))
|
|
937
|
+
else:
|
|
938
|
+
stacktail = self.gen_stack_descriptions()
|
|
939
|
+
lines.append("Current stack tail:")
|
|
940
|
+
lines.extend(f" {x}" for x in stacktail)
|
|
941
|
+
if hasattr(node, "stacktail"):
|
|
942
|
+
lines.append("Previous stack tail:")
|
|
943
|
+
lines.extend(f" {x}" for x in node.stacktail)
|
|
944
|
+
lines.append(f"Reason: {reason}")
|
|
945
|
+
lines.append("*** End Not Deterministic Debug ***")
|
|
946
|
+
for line in lines:
|
|
947
|
+
print(line)
|
|
948
|
+
exc = NotDeterministic()
|
|
949
|
+
if currently_handling:
|
|
950
|
+
raise exc from currently_handling
|
|
951
|
+
else:
|
|
952
|
+
raise exc
|
|
953
|
+
|
|
954
|
+
def find_model_value(self, expr: z3.ExprRef) -> Any:
|
|
955
|
+
with NoTracing():
|
|
956
|
+
while True:
|
|
957
|
+
if isinstance(self._search_position, NodeStem):
|
|
958
|
+
self._search_position = self.grow_into(
|
|
959
|
+
ModelValueNode(self._random, expr, self.solver)
|
|
960
|
+
)
|
|
961
|
+
node = self._search_position
|
|
962
|
+
if isinstance(node, SearchLeaf):
|
|
963
|
+
raise CrossHairInternal(
|
|
964
|
+
f"Cannot use symbolics; path is already terminated"
|
|
965
|
+
)
|
|
966
|
+
if not isinstance(node, ModelValueNode):
|
|
967
|
+
debug(" *** Begin Not Deterministic Debug *** ")
|
|
968
|
+
debug(f"Model value node expected; found {type(node)} instead.")
|
|
969
|
+
debug(" Traceback: ", ch_stack())
|
|
970
|
+
debug(" *** End Not Deterministic Debug *** ")
|
|
971
|
+
raise NotDeterministic
|
|
972
|
+
(chosen, _, next_node) = node.choose(self, probability_true=1.0)
|
|
973
|
+
self.choices_made.append(node)
|
|
974
|
+
self._search_position = next_node
|
|
975
|
+
if chosen:
|
|
976
|
+
self.solver.add(expr == node.condition_value)
|
|
977
|
+
ret = model_value_to_python(node.condition_value)
|
|
978
|
+
if (
|
|
979
|
+
in_debug()
|
|
980
|
+
and not self.is_detached
|
|
981
|
+
and expr not in self._already_logged
|
|
982
|
+
):
|
|
983
|
+
self._already_logged.add(expr)
|
|
984
|
+
debug("SMT realized symbolic:", expr, "==", repr(ret))
|
|
985
|
+
debug("Realized at", ch_stack())
|
|
986
|
+
return ret
|
|
987
|
+
else:
|
|
988
|
+
self.solver.add(expr != node.condition_value)
|
|
989
|
+
|
|
990
|
+
def find_model_value_for_function(self, expr: z3.ExprRef) -> object:
|
|
991
|
+
if not solver_is_sat(self.solver):
|
|
992
|
+
raise CrossHairInternal("model unexpectedly became unsatisfiable")
|
|
993
|
+
# TODO: this need to go into a tree node that returns UNKNOWN or worse
|
|
994
|
+
# (because it just returns one example function; it's not covering the space)
|
|
995
|
+
|
|
996
|
+
# TODO: note this is also unsound - after completion, the solver isn't
|
|
997
|
+
# bound to the returned interpretation. (but don't know how to add the
|
|
998
|
+
# right constraints) Maybe just use arrays instead.
|
|
999
|
+
return self.solver.model()[expr]
|
|
1000
|
+
|
|
1001
|
+
def current_snapshot(self) -> SnapshotRef:
|
|
1002
|
+
return SnapshotRef(len(self.heaps) - 1)
|
|
1003
|
+
|
|
1004
|
+
def checkpoint(self):
|
|
1005
|
+
self.heaps.append(
|
|
1006
|
+
[(ref, typ, copy.deepcopy(val)) for (ref, typ, val) in self.heaps[-1]]
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
def add_value_to_heaps(self, ref: z3.ExprRef, typ: Type, value: object) -> None:
|
|
1010
|
+
# TODO: needs more testing
|
|
1011
|
+
for heap in self.heaps[:-1]:
|
|
1012
|
+
heap.append((ref, typ, copy.deepcopy(value)))
|
|
1013
|
+
self.heaps[-1].append((ref, typ, value))
|
|
1014
|
+
|
|
1015
|
+
def find_key_in_heap(
|
|
1016
|
+
self,
|
|
1017
|
+
ref: z3.ExprRef,
|
|
1018
|
+
typ: Type,
|
|
1019
|
+
proxy_generator: Callable[[Type], object],
|
|
1020
|
+
snapshot: SnapshotRef = SnapshotRef(-1),
|
|
1021
|
+
) -> object:
|
|
1022
|
+
with NoTracing():
|
|
1023
|
+
# TODO: needs more testing
|
|
1024
|
+
for curref, curtyp, curval in self.heaps[snapshot]:
|
|
1025
|
+
|
|
1026
|
+
# TODO: using unify() is almost certainly wrong; just because the types
|
|
1027
|
+
# have some instances in common does not mean that `curval` actually
|
|
1028
|
+
# satisfies the requirements of `typ`:
|
|
1029
|
+
could_match = dynamic_typing.unify(curtyp, typ)
|
|
1030
|
+
if not could_match:
|
|
1031
|
+
continue
|
|
1032
|
+
if self.smt_fork(curref == ref, probability_true=0.1):
|
|
1033
|
+
debug(
|
|
1034
|
+
"Heap key lookup ",
|
|
1035
|
+
ref,
|
|
1036
|
+
": Found existing. ",
|
|
1037
|
+
"type:",
|
|
1038
|
+
name_of_type(type(curval)),
|
|
1039
|
+
"id:",
|
|
1040
|
+
id(curval) % 1000,
|
|
1041
|
+
)
|
|
1042
|
+
return curval
|
|
1043
|
+
ret = proxy_generator(typ)
|
|
1044
|
+
debug(
|
|
1045
|
+
"Heap key lookup ",
|
|
1046
|
+
ref,
|
|
1047
|
+
": Created new. ",
|
|
1048
|
+
"type:",
|
|
1049
|
+
name_of_type(type(ret)),
|
|
1050
|
+
"id:",
|
|
1051
|
+
id(ret) % 1000,
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
self.add_value_to_heaps(ref, typ, ret)
|
|
1055
|
+
return ret
|
|
1056
|
+
|
|
1057
|
+
def uniq(self):
|
|
1058
|
+
self.next_uniq += 1
|
|
1059
|
+
return "_{:x}".format(self.next_uniq)
|
|
1060
|
+
|
|
1061
|
+
@assert_tracing(False)
|
|
1062
|
+
def smt_fanout(
|
|
1063
|
+
self,
|
|
1064
|
+
exprs_and_results: Sequence[Tuple[z3.ExprRef, object]],
|
|
1065
|
+
desc: str,
|
|
1066
|
+
weights: Optional[Sequence[float]] = None,
|
|
1067
|
+
none_of_the_above_weight: float = 0.0,
|
|
1068
|
+
):
|
|
1069
|
+
"""Performs a weighted binary search over the given SMT expressions."""
|
|
1070
|
+
exprs = [e for (e, _) in exprs_and_results]
|
|
1071
|
+
final_weights = [1.0] * len(exprs) if weights is None else weights
|
|
1072
|
+
if CROSSHAIR_EXTRA_ASSERTS:
|
|
1073
|
+
if len(final_weights) != len(exprs):
|
|
1074
|
+
raise CrossHairInternal("inconsistent smt_fanout exprs and weights")
|
|
1075
|
+
if not all(0 < w for w in final_weights):
|
|
1076
|
+
raise CrossHairInternal("smt_fanout weights must be greater than zero")
|
|
1077
|
+
if not self.is_possible(z3Or(*exprs)):
|
|
1078
|
+
raise CrossHairInternal(
|
|
1079
|
+
"no smt_fanout option is possible: " + repr(exprs)
|
|
1080
|
+
)
|
|
1081
|
+
if self.is_possible(z3Not(z3Or(*exprs))):
|
|
1082
|
+
raise CrossHairInternal(
|
|
1083
|
+
"smt_fanout options are not exhaustive: " + repr(exprs)
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
def attempt(start: int, end: int):
|
|
1087
|
+
size = end - start
|
|
1088
|
+
if size == 1:
|
|
1089
|
+
return exprs_and_results[start][1]
|
|
1090
|
+
mid = (start + end) // 2
|
|
1091
|
+
left_exprs = exprs[start:mid]
|
|
1092
|
+
left_weight = sum(final_weights[start:mid])
|
|
1093
|
+
right_weight = sum(final_weights[mid:end])
|
|
1094
|
+
if self.smt_fork(
|
|
1095
|
+
z3Or(*left_exprs),
|
|
1096
|
+
probability_true=left_weight / (left_weight + right_weight),
|
|
1097
|
+
desc=f"{desc}_fan_size_{size}",
|
|
1098
|
+
):
|
|
1099
|
+
return attempt(start, mid)
|
|
1100
|
+
else:
|
|
1101
|
+
return attempt(mid, end)
|
|
1102
|
+
|
|
1103
|
+
return attempt(0, len(exprs))
|
|
1104
|
+
|
|
1105
|
+
@assert_tracing(False)
|
|
1106
|
+
def smt_fork(
|
|
1107
|
+
self,
|
|
1108
|
+
expr: Optional[z3.ExprRef] = None,
|
|
1109
|
+
desc: Optional[str] = None,
|
|
1110
|
+
probability_true: Optional[float] = None,
|
|
1111
|
+
) -> bool:
|
|
1112
|
+
if expr is None:
|
|
1113
|
+
expr = z3.Bool((desc or "fork") + self.uniq())
|
|
1114
|
+
return self.choose_possible(expr, probability_true)
|
|
1115
|
+
|
|
1116
|
+
def defer_assumption(self, description: str, checker: Callable[[], bool]) -> None:
|
|
1117
|
+
self._deferred_assumptions.append((description, checker))
|
|
1118
|
+
|
|
1119
|
+
def extend_timeouts(
|
|
1120
|
+
self, constant_factor: float = 0.0, smt_multiple: Optional[float] = None
|
|
1121
|
+
) -> None:
|
|
1122
|
+
self.execution_deadline += constant_factor
|
|
1123
|
+
if self.smt_timeout is not None and smt_multiple is not None:
|
|
1124
|
+
self.smt_timeout = int(self.smt_timeout * smt_multiple)
|
|
1125
|
+
self.solver.set(timeout=self.smt_timeout)
|
|
1126
|
+
|
|
1127
|
+
def detach_path(self, currently_handling: Optional[BaseException] = None) -> None:
|
|
1128
|
+
"""
|
|
1129
|
+
Mark the current path exhausted.
|
|
1130
|
+
|
|
1131
|
+
Also verifies all deferred assumptions.
|
|
1132
|
+
After detaching, the space may continue to be used (for example, to print
|
|
1133
|
+
realized symbolics).
|
|
1134
|
+
"""
|
|
1135
|
+
assert is_tracing()
|
|
1136
|
+
with NoTracing():
|
|
1137
|
+
if self.is_detached:
|
|
1138
|
+
debug("Path is already detached")
|
|
1139
|
+
return
|
|
1140
|
+
# Give ourselves a time extension for deferred assumptions and
|
|
1141
|
+
# (likely) counterexample generation to follow.
|
|
1142
|
+
self.extend_timeouts(constant_factor=4.0, smt_multiple=2.0)
|
|
1143
|
+
for description, checker in self._deferred_assumptions:
|
|
1144
|
+
with ResumedTracing():
|
|
1145
|
+
check_ret = checker()
|
|
1146
|
+
if not prefer_true(check_ret):
|
|
1147
|
+
raise IgnoreAttempt("deferred assumption failed: " + description)
|
|
1148
|
+
self.is_detached = True
|
|
1149
|
+
if not isinstance(self._search_position, NodeStem):
|
|
1150
|
+
self.raise_not_deterministic(
|
|
1151
|
+
self._search_position,
|
|
1152
|
+
f"Expect to detach path at a stem node, not at this node: {self._search_position}",
|
|
1153
|
+
currently_handling=currently_handling,
|
|
1154
|
+
)
|
|
1155
|
+
node = self.grow_into(DetachedPathNode())
|
|
1156
|
+
assert isinstance(node.child, NodeStem)
|
|
1157
|
+
self.choices_made.append(node)
|
|
1158
|
+
self._search_position = node.child
|
|
1159
|
+
debug("Detached from search tree")
|
|
1160
|
+
|
|
1161
|
+
def cap_result_at_unknown(self):
|
|
1162
|
+
# TODO: this doesn't seem to work as intended.
|
|
1163
|
+
# If any execution path is confirmed, the end result is sometimes confirmed as well.
|
|
1164
|
+
self.status_cap = VerificationStatus.UNKNOWN
|
|
1165
|
+
|
|
1166
|
+
def bubble_status(
|
|
1167
|
+
self, analysis: CallAnalysis
|
|
1168
|
+
) -> Tuple[Optional[CallAnalysis], bool]:
|
|
1169
|
+
# In some cases, we might ignore an attempt while not at a leaf.
|
|
1170
|
+
if isinstance(self._search_position, NodeStem):
|
|
1171
|
+
self._search_position = self.grow_into(SearchLeaf(analysis))
|
|
1172
|
+
else:
|
|
1173
|
+
assert isinstance(self._search_position, SearchTreeNode)
|
|
1174
|
+
self._search_position.exhausted = True
|
|
1175
|
+
self._search_position.result = analysis
|
|
1176
|
+
self._root.pathing_oracle.post_path_hook(self.choices_made)
|
|
1177
|
+
if not self.choices_made:
|
|
1178
|
+
return (analysis, True)
|
|
1179
|
+
for node in reversed(self.choices_made):
|
|
1180
|
+
node.update_result(analysis)
|
|
1181
|
+
if False: # this is more noise than it's worth (usually)
|
|
1182
|
+
if in_debug():
|
|
1183
|
+
for line in debug_path_tree(
|
|
1184
|
+
self._root, set(self.choices_made + [self._search_position])
|
|
1185
|
+
):
|
|
1186
|
+
debug(line)
|
|
1187
|
+
# debug('Path summary:', self.choices_made)
|
|
1188
|
+
first = self.choices_made[0]
|
|
1189
|
+
analysis = first.get_result()
|
|
1190
|
+
verification_status = analysis.verification_status
|
|
1191
|
+
if self.status_cap is not None and verification_status is not None:
|
|
1192
|
+
analysis.verification_status = min(verification_status, self.status_cap)
|
|
1193
|
+
return (analysis, first.is_exhausted())
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
class SimpleStateSpace(StateSpace):
|
|
1197
|
+
def __init__(self):
|
|
1198
|
+
super().__init__(monotonic() + 10000.0, 10000.0, RootNode())
|
|
1199
|
+
self.mark_all_parent_frames()
|