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.
Files changed (176) hide show
  1. _crosshair_tracers.cpython-312-darwin.so +0 -0
  2. crosshair/__init__.py +42 -0
  3. crosshair/__main__.py +8 -0
  4. crosshair/_mark_stacks.h +790 -0
  5. crosshair/_preliminaries_test.py +18 -0
  6. crosshair/_tracers.h +94 -0
  7. crosshair/_tracers_pycompat.h +522 -0
  8. crosshair/_tracers_test.py +138 -0
  9. crosshair/abcstring.py +245 -0
  10. crosshair/auditwall.py +190 -0
  11. crosshair/auditwall_test.py +77 -0
  12. crosshair/codeconfig.py +113 -0
  13. crosshair/codeconfig_test.py +117 -0
  14. crosshair/condition_parser.py +1237 -0
  15. crosshair/condition_parser_test.py +497 -0
  16. crosshair/conftest.py +30 -0
  17. crosshair/copyext.py +155 -0
  18. crosshair/copyext_test.py +84 -0
  19. crosshair/core.py +1763 -0
  20. crosshair/core_and_libs.py +149 -0
  21. crosshair/core_regestered_types_test.py +82 -0
  22. crosshair/core_test.py +1316 -0
  23. crosshair/diff_behavior.py +314 -0
  24. crosshair/diff_behavior_test.py +261 -0
  25. crosshair/dynamic_typing.py +346 -0
  26. crosshair/dynamic_typing_test.py +210 -0
  27. crosshair/enforce.py +282 -0
  28. crosshair/enforce_test.py +182 -0
  29. crosshair/examples/PEP316/__init__.py +1 -0
  30. crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
  31. crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
  32. crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
  33. crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
  34. crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
  35. crosshair/examples/PEP316/correct_code/__init__.py +0 -0
  36. crosshair/examples/PEP316/correct_code/arith.py +60 -0
  37. crosshair/examples/PEP316/correct_code/chess.py +77 -0
  38. crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
  39. crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
  40. crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
  41. crosshair/examples/PEP316/correct_code/showcase.py +104 -0
  42. crosshair/examples/__init__.py +0 -0
  43. crosshair/examples/check_examples_test.py +146 -0
  44. crosshair/examples/deal/__init__.py +1 -0
  45. crosshair/examples/icontract/__init__.py +1 -0
  46. crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
  47. crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
  48. crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
  49. crosshair/examples/icontract/correct_code/__init__.py +0 -0
  50. crosshair/examples/icontract/correct_code/arith.py +51 -0
  51. crosshair/examples/icontract/correct_code/showcase.py +94 -0
  52. crosshair/fnutil.py +391 -0
  53. crosshair/fnutil_test.py +75 -0
  54. crosshair/fuzz_core_test.py +516 -0
  55. crosshair/libimpl/__init__.py +0 -0
  56. crosshair/libimpl/arraylib.py +161 -0
  57. crosshair/libimpl/binascii_ch_test.py +30 -0
  58. crosshair/libimpl/binascii_test.py +67 -0
  59. crosshair/libimpl/binasciilib.py +150 -0
  60. crosshair/libimpl/bisectlib_test.py +23 -0
  61. crosshair/libimpl/builtinslib.py +5228 -0
  62. crosshair/libimpl/builtinslib_ch_test.py +1191 -0
  63. crosshair/libimpl/builtinslib_test.py +3735 -0
  64. crosshair/libimpl/codecslib.py +86 -0
  65. crosshair/libimpl/codecslib_test.py +86 -0
  66. crosshair/libimpl/collectionslib.py +264 -0
  67. crosshair/libimpl/collectionslib_ch_test.py +252 -0
  68. crosshair/libimpl/collectionslib_test.py +332 -0
  69. crosshair/libimpl/copylib.py +23 -0
  70. crosshair/libimpl/copylib_test.py +18 -0
  71. crosshair/libimpl/datetimelib.py +2559 -0
  72. crosshair/libimpl/datetimelib_ch_test.py +354 -0
  73. crosshair/libimpl/datetimelib_test.py +112 -0
  74. crosshair/libimpl/decimallib.py +5257 -0
  75. crosshair/libimpl/decimallib_ch_test.py +78 -0
  76. crosshair/libimpl/decimallib_test.py +76 -0
  77. crosshair/libimpl/encodings/__init__.py +23 -0
  78. crosshair/libimpl/encodings/_encutil.py +187 -0
  79. crosshair/libimpl/encodings/ascii.py +44 -0
  80. crosshair/libimpl/encodings/latin_1.py +40 -0
  81. crosshair/libimpl/encodings/utf_8.py +93 -0
  82. crosshair/libimpl/encodings_ch_test.py +83 -0
  83. crosshair/libimpl/fractionlib.py +16 -0
  84. crosshair/libimpl/fractionlib_test.py +80 -0
  85. crosshair/libimpl/functoolslib.py +34 -0
  86. crosshair/libimpl/functoolslib_test.py +56 -0
  87. crosshair/libimpl/hashliblib.py +30 -0
  88. crosshair/libimpl/hashliblib_test.py +18 -0
  89. crosshair/libimpl/heapqlib.py +47 -0
  90. crosshair/libimpl/heapqlib_test.py +21 -0
  91. crosshair/libimpl/importliblib.py +18 -0
  92. crosshair/libimpl/importliblib_test.py +38 -0
  93. crosshair/libimpl/iolib.py +216 -0
  94. crosshair/libimpl/iolib_ch_test.py +128 -0
  95. crosshair/libimpl/iolib_test.py +19 -0
  96. crosshair/libimpl/ipaddresslib.py +8 -0
  97. crosshair/libimpl/itertoolslib.py +44 -0
  98. crosshair/libimpl/itertoolslib_test.py +44 -0
  99. crosshair/libimpl/jsonlib.py +984 -0
  100. crosshair/libimpl/jsonlib_ch_test.py +42 -0
  101. crosshair/libimpl/jsonlib_test.py +51 -0
  102. crosshair/libimpl/mathlib.py +179 -0
  103. crosshair/libimpl/mathlib_ch_test.py +44 -0
  104. crosshair/libimpl/mathlib_test.py +67 -0
  105. crosshair/libimpl/oslib.py +7 -0
  106. crosshair/libimpl/pathliblib_test.py +10 -0
  107. crosshair/libimpl/randomlib.py +178 -0
  108. crosshair/libimpl/randomlib_test.py +120 -0
  109. crosshair/libimpl/relib.py +846 -0
  110. crosshair/libimpl/relib_ch_test.py +169 -0
  111. crosshair/libimpl/relib_test.py +493 -0
  112. crosshair/libimpl/timelib.py +72 -0
  113. crosshair/libimpl/timelib_test.py +82 -0
  114. crosshair/libimpl/typeslib.py +15 -0
  115. crosshair/libimpl/typeslib_test.py +36 -0
  116. crosshair/libimpl/unicodedatalib.py +75 -0
  117. crosshair/libimpl/unicodedatalib_test.py +42 -0
  118. crosshair/libimpl/urlliblib.py +23 -0
  119. crosshair/libimpl/urlliblib_test.py +19 -0
  120. crosshair/libimpl/weakreflib.py +13 -0
  121. crosshair/libimpl/weakreflib_test.py +69 -0
  122. crosshair/libimpl/zliblib.py +15 -0
  123. crosshair/libimpl/zliblib_test.py +13 -0
  124. crosshair/lsp_server.py +261 -0
  125. crosshair/lsp_server_test.py +30 -0
  126. crosshair/main.py +973 -0
  127. crosshair/main_test.py +543 -0
  128. crosshair/objectproxy.py +376 -0
  129. crosshair/objectproxy_test.py +41 -0
  130. crosshair/opcode_intercept.py +601 -0
  131. crosshair/opcode_intercept_test.py +304 -0
  132. crosshair/options.py +218 -0
  133. crosshair/options_test.py +10 -0
  134. crosshair/patch_equivalence_test.py +75 -0
  135. crosshair/path_cover.py +209 -0
  136. crosshair/path_cover_test.py +138 -0
  137. crosshair/path_search.py +161 -0
  138. crosshair/path_search_test.py +52 -0
  139. crosshair/pathing_oracle.py +271 -0
  140. crosshair/pathing_oracle_test.py +21 -0
  141. crosshair/pure_importer.py +27 -0
  142. crosshair/pure_importer_test.py +16 -0
  143. crosshair/py.typed +0 -0
  144. crosshair/register_contract.py +273 -0
  145. crosshair/register_contract_test.py +190 -0
  146. crosshair/simplestructs.py +1165 -0
  147. crosshair/simplestructs_test.py +283 -0
  148. crosshair/smtlib.py +24 -0
  149. crosshair/smtlib_test.py +14 -0
  150. crosshair/statespace.py +1199 -0
  151. crosshair/statespace_test.py +108 -0
  152. crosshair/stubs_parser.py +352 -0
  153. crosshair/stubs_parser_test.py +43 -0
  154. crosshair/test_util.py +329 -0
  155. crosshair/test_util_test.py +26 -0
  156. crosshair/tools/__init__.py +0 -0
  157. crosshair/tools/check_help_in_doc.py +264 -0
  158. crosshair/tools/check_init_and_setup_coincide.py +119 -0
  159. crosshair/tools/generate_demo_table.py +127 -0
  160. crosshair/tracers.py +544 -0
  161. crosshair/tracers_test.py +154 -0
  162. crosshair/type_repo.py +151 -0
  163. crosshair/unicode_categories.py +589 -0
  164. crosshair/unicode_categories_test.py +27 -0
  165. crosshair/util.py +741 -0
  166. crosshair/util_test.py +173 -0
  167. crosshair/watcher.py +307 -0
  168. crosshair/watcher_test.py +107 -0
  169. crosshair/z3util.py +76 -0
  170. crosshair/z3util_test.py +11 -0
  171. crosshair_tool-0.0.99.dist-info/METADATA +144 -0
  172. crosshair_tool-0.0.99.dist-info/RECORD +176 -0
  173. crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
  174. crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
  175. crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
  176. crosshair_tool-0.0.99.dist-info/top_level.txt +2 -0
@@ -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()