crosshair-tool 0.0.97__cp314-cp314-win32.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.

Potentially problematic release.


This version of crosshair-tool might be problematic. Click here for more details.

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