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
crosshair/core.py ADDED
@@ -0,0 +1,1763 @@
1
+ # TODO: drop to PDB option
2
+ # TODO: detect problems with backslashes in docstrings
3
+
4
+ # *** Not prioritized for v0 ***
5
+ # TODO: increase test coverage: TypeVar('T', int, str) vs bounded type vars
6
+ # TODO: consider raises conditions (guaranteed to raise, guaranteed to not raise?)
7
+ # TODO: precondition strengthening ban (Subclass constraint rule)
8
+ # TODO: mutating symbolic Callables?
9
+ # TODO: contracts on the contracts of function and object inputs/outputs?
10
+
11
+ import enum
12
+ import functools
13
+ import inspect
14
+ import linecache
15
+ import os.path
16
+ import sys
17
+ import time
18
+ import traceback
19
+ import types
20
+ import typing
21
+ from collections import ChainMap, defaultdict, deque
22
+ from contextlib import ExitStack
23
+ from dataclasses import dataclass, replace
24
+ from inspect import BoundArguments, Signature, isabstract
25
+ from time import monotonic
26
+ from traceback import StackSummary, extract_stack, extract_tb, format_exc
27
+ from typing import (
28
+ Any,
29
+ Callable,
30
+ Collection,
31
+ Dict,
32
+ FrozenSet,
33
+ Iterable,
34
+ List,
35
+ Mapping,
36
+ MutableMapping,
37
+ Optional,
38
+ Sequence,
39
+ Set,
40
+ Tuple,
41
+ Type,
42
+ TypeVar,
43
+ Union,
44
+ cast,
45
+ get_type_hints,
46
+ overload,
47
+ )
48
+
49
+ import typing_inspect # type: ignore
50
+ import z3 # type: ignore
51
+
52
+ from crosshair import dynamic_typing
53
+ from crosshair.codeconfig import collect_options
54
+ from crosshair.condition_parser import (
55
+ ConditionExpr,
56
+ ConditionExprType,
57
+ Conditions,
58
+ condition_parser,
59
+ get_current_parser,
60
+ )
61
+ from crosshair.copyext import CopyMode, deepcopyext
62
+ from crosshair.enforce import (
63
+ EnforcedConditions,
64
+ NoEnforce,
65
+ PostconditionFailed,
66
+ PreconditionFailed,
67
+ WithEnforcement,
68
+ )
69
+ from crosshair.fnutil import (
70
+ FunctionInfo,
71
+ get_top_level_classes_and_functions,
72
+ resolve_signature,
73
+ )
74
+ from crosshair.options import DEFAULT_OPTIONS, AnalysisOptions, AnalysisOptionSet
75
+ from crosshair.register_contract import clear_contract_registrations, get_contract
76
+ from crosshair.statespace import (
77
+ AnalysisMessage,
78
+ CallAnalysis,
79
+ MessageType,
80
+ RootNode,
81
+ SimpleStateSpace,
82
+ StateSpace,
83
+ StateSpaceContext,
84
+ VerificationStatus,
85
+ context_statespace,
86
+ optional_context_statespace,
87
+ prefer_true,
88
+ )
89
+ from crosshair.tracers import (
90
+ COMPOSITE_TRACER,
91
+ CompositeTracer,
92
+ NoTracing,
93
+ PatchingModule,
94
+ ResumedTracing,
95
+ TracingModule,
96
+ check_opcode_support,
97
+ is_tracing,
98
+ )
99
+ from crosshair.type_repo import get_subclass_map
100
+ from crosshair.util import (
101
+ ATOMIC_IMMUTABLE_TYPES,
102
+ UNABLE_TO_REPR_TEXT,
103
+ AttributeHolder,
104
+ CrossHairInternal,
105
+ CrosshairUnsupported,
106
+ CrossHairValue,
107
+ EvalFriendlyReprContext,
108
+ IdKeyedDict,
109
+ IgnoreAttempt,
110
+ NotDeterministic,
111
+ ReferencedIdentifier,
112
+ UnexploredPath,
113
+ ch_stack,
114
+ debug,
115
+ eval_friendly_repr,
116
+ format_boundargs,
117
+ frame_summary_for_fn,
118
+ in_debug,
119
+ method_identifier,
120
+ name_of_type,
121
+ origin_of,
122
+ renamed_function,
123
+ samefile,
124
+ smtlib_typename,
125
+ sourcelines,
126
+ type_args_of,
127
+ warn,
128
+ )
129
+
130
+ if sys.version_info >= (3, 12):
131
+ from typing import TypeAliasType
132
+
133
+ TypeAliasTypes = (TypeAliasType,)
134
+ else:
135
+ TypeAliasTypes = ()
136
+
137
+
138
+ _MISSING = object()
139
+
140
+
141
+ _OPCODE_PATCHES: List[TracingModule] = []
142
+
143
+ _PATCH_REGISTRATIONS: Dict[Callable, Callable] = {}
144
+
145
+
146
+ class Patched:
147
+ def __enter__(self):
148
+ COMPOSITE_TRACER.patching_module.add(_PATCH_REGISTRATIONS)
149
+ if len(_OPCODE_PATCHES) == 0:
150
+ raise CrossHairInternal("Opcode patches haven't been loaded yet.")
151
+ for module in _OPCODE_PATCHES:
152
+ COMPOSITE_TRACER.push_module(module)
153
+ self.pushed = _OPCODE_PATCHES[:]
154
+ return self
155
+
156
+ def __exit__(self, exc_type, exc_val, exc_tb):
157
+ for module in reversed(self.pushed):
158
+ COMPOSITE_TRACER.pop_config(module)
159
+ COMPOSITE_TRACER.patching_module.pop(_PATCH_REGISTRATIONS)
160
+ return False
161
+
162
+
163
+ class _StandaloneStatespace(ExitStack):
164
+ def __enter__(self) -> StateSpace: # type: ignore
165
+ # We explicitly don't set up contexts to enforce conditions - that's because
166
+ # conditions involve a choice, and standalone_statespace is for testing that
167
+ # does not require making any choices.
168
+ super().__enter__()
169
+ space = SimpleStateSpace()
170
+ self.enter_context(condition_parser(DEFAULT_OPTIONS.analysis_kind))
171
+ self.enter_context(Patched())
172
+ self.enter_context(StateSpaceContext(space))
173
+ COMPOSITE_TRACER.trace_caller()
174
+ self.enter_context(COMPOSITE_TRACER)
175
+ return space
176
+
177
+
178
+ standalone_statespace = _StandaloneStatespace()
179
+
180
+
181
+ def suspected_proxy_intolerance_exception(exc_value: Exception) -> bool:
182
+ # NOTE: this is an intentionally very hacky function that is used to
183
+ # skip iterations where a symbolic is used in some function that can't
184
+ # accept it.
185
+ # As the standard library gets more and more support, this is
186
+ # less necessary.
187
+ # Although it would still provide value for 3rd party libraries
188
+ # implemented in C, the long-term goal is to remove it and just let
189
+ # CrossHair be noisy where it isn't supported.
190
+
191
+ if not isinstance(exc_value, TypeError):
192
+ return False
193
+ exc_str = str(exc_value)
194
+ atomic_symbolic = "SymbolicInt" in exc_str or "SymbolicFloat" in exc_str
195
+ if (
196
+ atomic_symbolic
197
+ or "SymbolicStr" in exc_str
198
+ or "__hash__ method should return an integer" in exc_str
199
+ or "expected string or bytes-like object" in exc_str
200
+ ):
201
+ if (
202
+ "can only concatenate" in exc_str
203
+ or "NoneType" in exc_str
204
+ or "object is not callable" in exc_str
205
+ ):
206
+ # https://github.com/pschanely/CrossHair/issues/234
207
+ # (the three conditions above correspond to examples 2, 3, and 4)
208
+ return False
209
+ if atomic_symbolic and "object is not iterable" in exc_str:
210
+ # https://github.com/pschanely/CrossHair/issues/322
211
+ return False
212
+ return True
213
+ return False
214
+
215
+
216
+ class ExceptionFilter:
217
+ analysis: CallAnalysis
218
+ ignore: bool = False
219
+ ignore_with_confirmation: bool = False
220
+ user_exc: Optional[Tuple[BaseException, StackSummary]] = None
221
+ expected_exceptions: Tuple[Type[BaseException], ...]
222
+
223
+ def __init__(
224
+ self, expected_exceptions: FrozenSet[Type[BaseException]] = frozenset()
225
+ ):
226
+ self.expected_exceptions = (NotImplementedError,) + tuple(expected_exceptions)
227
+
228
+ def has_user_exception(self) -> bool:
229
+ return self.user_exc is not None
230
+
231
+ def __enter__(self) -> "ExceptionFilter":
232
+ return self
233
+
234
+ def __exit__(self, exc_type, exc_value, tb) -> bool:
235
+ with NoTracing():
236
+ if isinstance(exc_value, (PostconditionFailed, IgnoreAttempt)):
237
+ if isinstance(exc_value, PostconditionFailed):
238
+ # Postcondition : although this indicates a problem, it's with a
239
+ # subroutine; not this function.
240
+ # Usualy we want to ignore this because it will be surfaced more locally
241
+ # in the subroutine.
242
+ debug(
243
+ f"Ignoring based on internal failed post condition: {exc_value}"
244
+ )
245
+ self.ignore = True
246
+ self.analysis = CallAnalysis()
247
+ return True
248
+ if isinstance(exc_value, self.expected_exceptions):
249
+ exc_type_name = type(exc_value).__name__
250
+ debug(f"Hit expected exception: {exc_type_name}: {exc_value}")
251
+ self.ignore = True
252
+ self.analysis = CallAnalysis(VerificationStatus.CONFIRMED)
253
+ return True
254
+ if suspected_proxy_intolerance_exception(exc_value):
255
+ # Ideally we'd attempt literal strings after encountering this.
256
+ # See https://github.com/pschanely/CrossHair/issues/8
257
+ debug("Proxy intolerace:", exc_value, "at", format_exc())
258
+ raise CrosshairUnsupported("Detected proxy intolerance")
259
+ if isinstance(exc_value, (Exception, PreconditionFailed)):
260
+ if isinstance(
261
+ exc_value,
262
+ (
263
+ z3.Z3Exception, # internal issue, re-raise
264
+ NotDeterministic, # cannot continue to use the solver, re-raise
265
+ ),
266
+ ):
267
+ return False
268
+ # Most other issues are assumed to be user-facing exceptions:
269
+ lower_frames = extract_tb(sys.exc_info()[2])
270
+ higher_frames = extract_stack()[:-2]
271
+ self.user_exc = (exc_value, StackSummary(higher_frames + lower_frames))
272
+ self.analysis = CallAnalysis(VerificationStatus.REFUTED)
273
+ return True # suppress user-level exception
274
+ return False # re-raise resource and system issues
275
+
276
+
277
+ _T = TypeVar("_T")
278
+
279
+
280
+ def realize(value: Any) -> Any:
281
+ with NoTracing():
282
+ if hasattr(type(value), "__ch_realize__"):
283
+ return value.__ch_realize__() # type: ignore
284
+ else:
285
+ return value
286
+
287
+
288
+ def deep_realize(value: _T, memo: Optional[Dict] = None) -> _T:
289
+ with NoTracing():
290
+ return deepcopyext(value, CopyMode.REALIZE, {} if memo is None else memo)
291
+
292
+
293
+ def normalize_pytype(typ: Type) -> Type:
294
+ if typing_inspect.is_typevar(typ):
295
+ # we treat type vars in the most general way possible (the bound, or as 'object')
296
+ bound = typing_inspect.get_bound(typ)
297
+ if bound is not None:
298
+ return normalize_pytype(bound)
299
+ constraints = typing_inspect.get_constraints(typ)
300
+ if constraints:
301
+ raise CrosshairUnsupported
302
+ # TODO: not easy; interpreting as a Union allows the type to be
303
+ # instantiated differently in different places. So, this doesn't work:
304
+ # return Union.__getitem__(tuple(map(normalize_pytype, constraints)))
305
+ return object
306
+ if typ is Any:
307
+ # The distinction between any and object is for type checking, crosshair treats them the same
308
+ return object
309
+ if typ is Type:
310
+ return type
311
+ return typ
312
+
313
+
314
+ def python_type(o: object) -> Type:
315
+ if is_tracing():
316
+ raise CrossHairInternal("should not be tracing while getting pytype")
317
+ if hasattr(type(o), "__ch_pytype__"):
318
+ obj_type = o.__ch_pytype__() # type: ignore
319
+ if hasattr(obj_type, "__origin__"):
320
+ obj_type = obj_type.__origin__
321
+ return obj_type
322
+ else:
323
+ return type(o)
324
+
325
+
326
+ def class_with_realized_methods(cls: _T) -> _T:
327
+ overrides = {
328
+ method_name: with_realized_args(method)
329
+ for method_name, method in inspect.getmembers(cls)
330
+ if callable(method) and not method_name.startswith("_")
331
+ }
332
+ return type(cls.__name__, (cls,), overrides) # type: ignore
333
+
334
+
335
+ def with_realized_args(fn: Callable, deep=False) -> Callable:
336
+ realize_fn = deep_realize if deep else realize
337
+
338
+ def realizer(*a, **kw):
339
+ with NoTracing():
340
+ a = [realize_fn(arg) for arg in a]
341
+ kw = {k: realize_fn(v) for (k, v) in kw.items()}
342
+ # You might think we don't need tracing here, but some operations can invoke user-defined behavior:
343
+ return fn(*a, **kw)
344
+
345
+ functools.update_wrapper(realizer, fn)
346
+ return realizer
347
+
348
+
349
+ def with_checked_self(pytype, method_name):
350
+ # This is used to patch methods on native python types to handle
351
+ # the (unlikely) possibility of them getting called on a symbolic
352
+ # directly (e.g. `map(dict.pop, ...)`)
353
+ #
354
+ # Generally, we apply this patch when the method takes no arguments
355
+ # and has a meaningful return value.
356
+ native_method = getattr(pytype, method_name)
357
+
358
+ def with_checked_self(self, *a, **kw):
359
+ with NoTracing():
360
+ if hasattr(self, "__ch_pytype__"):
361
+ if python_type(self) is pytype:
362
+ bound_method = getattr(self, method_name)
363
+ with ResumedTracing():
364
+ return bound_method(*a, **kw)
365
+ return native_method(self, *a, **kw)
366
+
367
+ functools.update_wrapper(with_checked_self, native_method)
368
+ return with_checked_self
369
+
370
+
371
+ def with_symbolic_self(symbolic_cls: Type, fn: Callable):
372
+ def call_with_symbolic_self(self, *args, **kwargs):
373
+ with NoTracing():
374
+ if isinstance(self, symbolic_cls):
375
+ # Handles (unlikely!) cases like str.isspace(<symbolic string>)
376
+ target_fn = getattr(symbolic_cls, fn.__name__)
377
+ elif any(isinstance(a, CrossHairValue) for a in args) or (
378
+ kwargs and any(isinstance(a, CrossHairValue) for a in kwargs.values())
379
+ ):
380
+ # NOTE: _ch_create_from_literal is suppoerted for very few types right now
381
+ self = symbolic_cls._ch_create_from_literal(self)
382
+ target_fn = getattr(symbolic_cls, fn.__name__)
383
+ else:
384
+ args = map(realize, args)
385
+ kwargs = {k: realize(v) for (k, v) in kwargs.items()}
386
+ target_fn = fn
387
+ return target_fn(self, *args, **kwargs)
388
+
389
+ functools.update_wrapper(call_with_symbolic_self, fn)
390
+ return call_with_symbolic_self
391
+
392
+
393
+ def with_uniform_probabilities(
394
+ collection: Collection[_T],
395
+ ) -> List[Tuple[_T, float]]:
396
+ count = len(collection)
397
+ return [(item, 1.0 / (count - idx)) for (idx, item) in enumerate(collection)]
398
+
399
+
400
+ def iter_types(from_type: Type, include_abstract: bool) -> List[Tuple[Type, float]]:
401
+ types = []
402
+ queue = deque([from_type])
403
+ subclassmap = get_subclass_map()
404
+ while queue:
405
+ cur = queue.popleft()
406
+ queue.extend(subclassmap[cur])
407
+ if include_abstract or not isabstract(cur):
408
+ types.append(cur)
409
+ ret = with_uniform_probabilities(types)
410
+ if ret and ret[0][0] is from_type:
411
+ # Bias a little extra for the base type;
412
+ # e.g. pick `int` more readily than the subclasses of int:
413
+ first_probability = ret[0][1]
414
+ ret[0] = (from_type, (first_probability + 3.0) / 4.0)
415
+ return ret
416
+
417
+
418
+ def choose_type(space: StateSpace, from_type: Type, varname: str) -> Optional[Type]:
419
+ pairs = iter_types(from_type, include_abstract=False)
420
+ if not pairs:
421
+ return None
422
+ for typ, probability_true in pairs:
423
+ # true_probability=1.0 does not guarantee selection
424
+ # (in particular, when the true path is exhausted)
425
+ if probability_true == 1.0:
426
+ return typ
427
+ if space.smt_fork(
428
+ desc=f"{varname}_is_{smtlib_typename(typ)}",
429
+ probability_true=probability_true,
430
+ ):
431
+ return typ
432
+ raise CrossHairInternal
433
+
434
+
435
+ def get_constructor_signature(cls: Type) -> Optional[inspect.Signature]:
436
+ # pydantic sets __signature__ on the class, so we look for that as well as on
437
+ # __init__ (see https://github.com/samuelcolvin/pydantic/pull/1034)
438
+ if hasattr(cls, "__signature__"):
439
+ sig = resolve_signature(cls)
440
+ if isinstance(sig, inspect.Signature):
441
+ return sig
442
+
443
+ applicable_sigs: List[Signature] = []
444
+ new_fn = cls.__new__
445
+ if new_fn is not object.__new__:
446
+ sig = resolve_signature(new_fn)
447
+ if not isinstance(sig, str):
448
+ applicable_sigs.append(sig)
449
+ init_fn = cls.__init__
450
+ if init_fn is not object.__init__:
451
+ sig = resolve_signature(init_fn)
452
+ if not isinstance(sig, str):
453
+ sig = sig.replace(
454
+ return_annotation=object
455
+ ) # make return types compatible (& use __new__'s return)
456
+ applicable_sigs.append(sig)
457
+ if len(applicable_sigs) == 0:
458
+ return inspect.Signature([])
459
+ if len(applicable_sigs) == 2:
460
+ sig = dynamic_typing.intersect_signatures(*applicable_sigs)
461
+ else:
462
+ sig = applicable_sigs[0]
463
+ # strip first argument ("self" or "cls")
464
+ newparams = list(sig.parameters.values())[1:]
465
+ return sig.replace(parameters=newparams)
466
+
467
+
468
+ _TYPE_HINTS = IdKeyedDict()
469
+
470
+
471
+ def proxy_for_class(typ: Type, varname: str) -> object:
472
+ data_members = _TYPE_HINTS.get(typ, None)
473
+ if data_members is None:
474
+ data_members = get_type_hints(typ)
475
+ _TYPE_HINTS[typ] = data_members
476
+
477
+ if sys.version_info >= (3, 8) and type(typ) is typing._TypedDictMeta: # type: ignore
478
+ # Handling for TypedDict
479
+ optional_keys = getattr(typ, "__optional_keys__", ())
480
+ keys = (
481
+ k
482
+ for k in data_members.keys()
483
+ if k not in optional_keys or context_statespace().smt_fork()
484
+ )
485
+ return {k: proxy_for_type(data_members[k], varname + "." + k) for k in keys}
486
+
487
+ constructor_sig = get_constructor_signature(typ)
488
+ if constructor_sig is None:
489
+ raise CrosshairUnsupported(
490
+ f"unable to create concrete instance of {typ} due to bad constructor"
491
+ )
492
+ # TODO: use dynamic_typing.get_bindings_from_type_arguments(typ) to instantiate
493
+ # type variables in `constructor_sig`
494
+ args = gen_args(constructor_sig)
495
+ typename = name_of_type(typ)
496
+ try:
497
+ with ResumedTracing():
498
+ obj = WithEnforcement(typ)(*args.args, **args.kwargs)
499
+ except (PreconditionFailed, PostconditionFailed):
500
+ # preconditions can be invalidated when the __init__ method has preconditions.
501
+ # postconditions can be invalidated when the class has invariants.
502
+ raise IgnoreAttempt
503
+ except Exception as e:
504
+ debug("Root-cause type construction traceback:", ch_stack(currently_handling=e))
505
+ raise CrosshairUnsupported(
506
+ f"error constructing {typename} instance: {name_of_type(type(e))}: {e}",
507
+ ) from e
508
+
509
+ debug("Proxy as a concrete instance of", typename)
510
+ reprer = context_statespace().extra(LazyCreationRepr)
511
+
512
+ def regenerate_construction_string(_):
513
+ with NoTracing():
514
+ realized_args = reprer.deep_realize(args)
515
+
516
+ return f"{repr(typ)}({format_boundargs(realized_args)})"
517
+
518
+ reprer.reprs[obj] = regenerate_construction_string
519
+ return obj
520
+
521
+
522
+ def register_patch(entity: Callable, patch_value: Callable):
523
+ if entity in _PATCH_REGISTRATIONS:
524
+ raise CrossHairInternal(f"Doubly registered patch: {entity}")
525
+ _PATCH_REGISTRATIONS[entity] = patch_value
526
+
527
+
528
+ def _reset_all_registrations():
529
+ global _SIMPLE_PROXIES
530
+ _SIMPLE_PROXIES.clear()
531
+ global _PATCH_REGISTRATIONS
532
+ _PATCH_REGISTRATIONS.clear()
533
+ global _OPCODE_PATCHES
534
+ _OPCODE_PATCHES.clear()
535
+ clear_contract_registrations()
536
+
537
+
538
+ def register_opcode_patch(module: TracingModule) -> None:
539
+ if type(module) in map(type, _OPCODE_PATCHES):
540
+ raise CrossHairInternal(
541
+ f"Doubly registered opcode patch module type: {type(module)}"
542
+ )
543
+ check_opcode_support(module.opcodes_wanted)
544
+
545
+ _OPCODE_PATCHES.append(module)
546
+
547
+
548
+ class SymbolicFactory:
549
+ """
550
+ A callable object that creates symbolic values.
551
+
552
+ .. automethod:: __call__
553
+ """
554
+
555
+ def __init__(self, space: StateSpace, pytype: object, varname: str):
556
+ self.space = space
557
+ self.pytype: Any = pytype
558
+ self.varname = varname
559
+
560
+ def get_suffixed_varname(self, suffix: str):
561
+ return self.varname + suffix + self.space.uniq()
562
+
563
+ @overload
564
+ def __call__(
565
+ self, typ: Callable[..., _T], suffix: str = "", allow_subtypes: bool = True
566
+ ) -> _T: ...
567
+
568
+ @overload
569
+ def __call__(
570
+ self, typ: Any, suffix: str = "", allow_subtypes: bool = True
571
+ ) -> Any: ...
572
+
573
+ def __call__(self, typ, suffix: str = "", allow_subtypes: bool = True):
574
+ """
575
+ Create a new symbolic value.
576
+
577
+ :param typ: The corresponding Python type for the returned symbolic.
578
+ :type typ: type
579
+ :param suffix: A descriptive suffix used to name variable(s) in the solver.
580
+ :type suffix: str
581
+ :param allow_subtypes: Whether it's ok to return a subtype of given type.
582
+ :type allow_subtypes: bool
583
+ :returns: A new symbolic value.
584
+ """
585
+ return proxy_for_type(
586
+ typ,
587
+ self.get_suffixed_varname(suffix),
588
+ allow_subtypes=allow_subtypes,
589
+ )
590
+
591
+
592
+ _SIMPLE_PROXIES: MutableMapping[type, Callable] = {}
593
+
594
+ SymbolicCreationCallback = Union[
595
+ # Sadly Callable[] doesn't support variable arguments. Just enumerate:
596
+ Callable[[SymbolicFactory], object],
597
+ Callable[[SymbolicFactory, Type], object],
598
+ Callable[[SymbolicFactory, Type, Type], object],
599
+ Callable[[SymbolicFactory, Type, Type, Type], object],
600
+ Callable[[SymbolicFactory, Type, Type, Type, Type], object],
601
+ ]
602
+
603
+
604
+ def register_type(typ: Type, creator: SymbolicCreationCallback) -> None:
605
+ """
606
+ Register a custom creation function to create symbolic values for a type.
607
+
608
+ :param typ: The Python type (or typing annotation) to handle.
609
+ :param creator: A function that takes a :class:`SymbolicFactory` instance and
610
+ returns a symbolic value. When creating a parameterized type (e.g. List[int]),
611
+ type parameters will be given to `creator` as additional arguments following the
612
+ factory.
613
+ """
614
+ assert typ is origin_of(
615
+ typ
616
+ ), f'Only origin types may be registered, not "{typ}": try "{origin_of(typ)}" instead.'
617
+ if typ in _SIMPLE_PROXIES:
618
+ raise CrossHairInternal(f'Duplicate type "{typ}" registered')
619
+ _SIMPLE_PROXIES[typ] = creator
620
+
621
+
622
+ @dataclass
623
+ class LazyCreationRepr:
624
+ def __init__(self, *a) -> None:
625
+ self.reprs = IdKeyedDict()
626
+ self.repr_references: Set[ReferencedIdentifier] = set()
627
+
628
+ def deep_realize(self, symbolic_val: object) -> Any:
629
+ assert not is_tracing()
630
+ reprs = self.reprs
631
+ arg_memo: dict = {}
632
+ realized_val = deepcopyext(symbolic_val, CopyMode.REALIZE, arg_memo)
633
+ for orig_id, new_obj in arg_memo.items():
634
+ old_repr = reprs.inner.get(orig_id, None)
635
+ if old_repr:
636
+ reprs.inner[id(new_obj)] = old_repr
637
+ return realized_val
638
+
639
+ def eval_friendly_format(
640
+ self, obj: _T, result_formatter: Callable[[_T], str]
641
+ ) -> str:
642
+ assert is_tracing()
643
+ with NoTracing():
644
+ obj = self.deep_realize(obj)
645
+ with EvalFriendlyReprContext(self.reprs) as ctx:
646
+ args_string = result_formatter(obj)
647
+ self.repr_references |= ctx.repr_references
648
+ return ctx.cleanup(args_string)
649
+
650
+
651
+ @overload
652
+ def proxy_for_type(
653
+ typ: Callable[..., _T],
654
+ varname: str,
655
+ allow_subtypes: bool = False,
656
+ ) -> _T: ...
657
+
658
+
659
+ @overload
660
+ def proxy_for_type(
661
+ typ: Any,
662
+ varname: str,
663
+ allow_subtypes: bool = False,
664
+ ) -> Any: ...
665
+
666
+
667
+ def proxy_for_type(
668
+ typ: Any,
669
+ varname: str,
670
+ allow_subtypes: bool = False,
671
+ ) -> Any:
672
+ space = context_statespace()
673
+ with NoTracing():
674
+ typ = normalize_pytype(typ)
675
+ origin = origin_of(typ)
676
+ type_args = type_args_of(typ)
677
+ while isinstance(origin, TypeAliasTypes):
678
+ type_var_bindings = dict(zip(origin.__type_params__, type_args))
679
+ unified = dynamic_typing.realize(origin.__value__, type_var_bindings)
680
+ return proxy_for_type(unified, varname, allow_subtypes)
681
+
682
+ # special cases
683
+ if isinstance(typ, type) and issubclass(typ, enum.Enum):
684
+ enum_values = list(typ) # type:ignore
685
+ if not enum_values:
686
+ raise IgnoreAttempt("No values for enum")
687
+ for enum_value in enum_values[:-1]:
688
+ if space.smt_fork(desc="choose_enum_" + str(enum_value)):
689
+ return enum_value
690
+ return enum_values[-1]
691
+ if not _SIMPLE_PROXIES:
692
+ from crosshair.core_and_libs import _make_registrations
693
+
694
+ _make_registrations()
695
+ proxy_factory = _SIMPLE_PROXIES.get(origin)
696
+ if proxy_factory:
697
+ recursive_proxy_factory = SymbolicFactory(space, typ, varname)
698
+ return proxy_factory(recursive_proxy_factory, *type_args)
699
+ if hasattr(typ, "__supertype__") and typing_inspect.is_new_type(typ):
700
+ return proxy_for_type(typ.__supertype__, varname, allow_subtypes) # type: ignore
701
+ if allow_subtypes and typ is not object:
702
+ typ = choose_type(space, typ, varname)
703
+ if typ is None: # (happens if typ and all subtypes are abstract)
704
+ raise IgnoreAttempt
705
+ return proxy_for_class(typ, varname)
706
+
707
+
708
+ _ARG_GENERATION_RENAMES: Dict[str, Callable] = {}
709
+
710
+
711
+ def gen_args(sig: inspect.Signature) -> inspect.BoundArguments:
712
+ if is_tracing():
713
+ raise CrossHairInternal
714
+ args = sig.bind_partial()
715
+ space = context_statespace()
716
+ for param in sig.parameters.values():
717
+ smt_name = param.name + space.uniq()
718
+ allow_subtypes = True
719
+
720
+ # For each argument, we call a special version of `proxy_for_type` that
721
+ # includes the argument name in the function name.
722
+ # This is nice while debugging stack traces, but also helps (e.g.)
723
+ # `CoveragePathingOracle` distinguish the decisions for each argument.
724
+ proxy_maker = _ARG_GENERATION_RENAMES.get(param.name)
725
+ if not proxy_maker:
726
+ if sys.version_info < (3, 8):
727
+ proxy_maker = proxy_for_type
728
+ else:
729
+ proxy_maker = renamed_function(proxy_for_type, "proxy_arg_" + param.name) # type: ignore
730
+ _ARG_GENERATION_RENAMES[param.name] = proxy_maker
731
+
732
+ has_annotation = param.annotation != inspect.Parameter.empty
733
+ value: object
734
+ if param.kind == inspect.Parameter.VAR_POSITIONAL:
735
+ if has_annotation:
736
+ varargs_type = List[param.annotation] # type: ignore
737
+ value = proxy_maker(varargs_type, smt_name, allow_subtypes)
738
+ else:
739
+ value = proxy_maker(List[Any], smt_name, allow_subtypes)
740
+ elif param.kind == inspect.Parameter.VAR_KEYWORD:
741
+ if has_annotation:
742
+ varargs_type = Dict[str, param.annotation] # type: ignore
743
+ value = cast(dict, proxy_maker(varargs_type, smt_name, allow_subtypes))
744
+ # Using ** on a dict requires concrete string keys. Force
745
+ # instiantiation of keys here:
746
+ value = {k.__str__(): v for (k, v) in value.items()}
747
+ else:
748
+ value = proxy_maker(Dict[str, Any], smt_name, allow_subtypes)
749
+ else:
750
+ is_self = param.name == "self"
751
+ # Object parameters can be any valid subtype iff they are not the
752
+ # class under test ("self").
753
+ allow_subtypes = not is_self
754
+ if has_annotation:
755
+ value = proxy_maker(param.annotation, smt_name, allow_subtypes)
756
+ else:
757
+ value = proxy_maker(cast(type, Any), smt_name, allow_subtypes)
758
+ if in_debug():
759
+ debug(
760
+ "created proxy for",
761
+ param.name,
762
+ "as type:",
763
+ name_of_type(type(value)),
764
+ hex(id(value)),
765
+ )
766
+ args.arguments[param.name] = value
767
+ return args
768
+
769
+
770
+ def message_sort_key(m: AnalysisMessage) -> tuple:
771
+ return (m.state, UNABLE_TO_REPR_TEXT not in m.message, -len(m.message))
772
+
773
+
774
+ class MessageCollector:
775
+ def __init__(self):
776
+ self.by_pos = {}
777
+
778
+ def extend(self, messages: Iterable[AnalysisMessage]) -> None:
779
+ for message in messages:
780
+ self.append(message)
781
+
782
+ def append(self, message: AnalysisMessage) -> None:
783
+ key = (message.filename, message.line, message.column)
784
+ if key in self.by_pos:
785
+ self.by_pos[key] = max(self.by_pos[key], message, key=message_sort_key)
786
+ else:
787
+ self.by_pos[key] = message
788
+
789
+ def get(self) -> List[AnalysisMessage]:
790
+ return [m for (k, m) in sorted(self.by_pos.items())]
791
+
792
+
793
+ class Checkable:
794
+ def analyze(self) -> Iterable[AnalysisMessage]:
795
+ raise NotImplementedError
796
+
797
+
798
+ @dataclass
799
+ class ConditionCheckable(Checkable):
800
+ ctxfn: FunctionInfo
801
+ options: AnalysisOptions
802
+ conditions: Conditions
803
+
804
+ def analyze(self) -> Iterable[AnalysisMessage]:
805
+ options = self.options
806
+ conditions = self.conditions
807
+ debug('Analyzing postcondition: "', conditions.post[0].expr_source, '"')
808
+ debug(
809
+ "assuming preconditions: ",
810
+ ",".join([p.expr_source for p in conditions.pre]),
811
+ )
812
+ options.deadline = monotonic() + options.per_condition_timeout
813
+
814
+ with condition_parser(options.analysis_kind):
815
+ analysis = analyze_calltree(options, conditions)
816
+
817
+ (condition,) = conditions.post
818
+ if analysis.verification_status is VerificationStatus.UNKNOWN:
819
+ message = "Not confirmed."
820
+ analysis.messages = [
821
+ AnalysisMessage(
822
+ MessageType.CANNOT_CONFIRM,
823
+ message,
824
+ condition.filename,
825
+ condition.line,
826
+ 0,
827
+ "",
828
+ )
829
+ ]
830
+ elif analysis.verification_status is VerificationStatus.CONFIRMED:
831
+ message = "Confirmed over all paths."
832
+ analysis.messages = [
833
+ AnalysisMessage(
834
+ MessageType.CONFIRMED,
835
+ message,
836
+ condition.filename,
837
+ condition.line,
838
+ 0,
839
+ "",
840
+ )
841
+ ]
842
+
843
+ return analysis.messages
844
+
845
+
846
+ class ClampedCheckable(Checkable):
847
+ """
848
+ Clamp messages for a class method to appear on the class itself.
849
+
850
+ So, even if the method is defined on a superclass, or defined dynamically (via
851
+ decorator etc), we report it on the class definition instead.
852
+ """
853
+
854
+ def __init__(self, checkable: Checkable, cls: type):
855
+ self.checkable = checkable
856
+ filename, start_line, _ = sourcelines(cls)
857
+ self.cls_file = filename
858
+ self.cls_start_line = start_line
859
+
860
+ def __repr__(self) -> str:
861
+ return f"ClampedCheckable({self.checkable})"
862
+
863
+ def analyze(self) -> Iterable[AnalysisMessage]:
864
+ cls_file = self.cls_file
865
+ ret = []
866
+ for message in self.checkable.analyze():
867
+ if not samefile(message.filename, cls_file):
868
+ ret.append(
869
+ replace(message, filename=cls_file, line=self.cls_start_line)
870
+ )
871
+ else:
872
+ ret.append(message)
873
+ return ret
874
+
875
+
876
+ @dataclass
877
+ class SyntaxErrorCheckable(Checkable):
878
+ messages: List[AnalysisMessage]
879
+
880
+ def analyze(self) -> Iterable[AnalysisMessage]:
881
+ return self.messages
882
+
883
+
884
+ def run_checkables(checkables: Iterable[Checkable]) -> List[AnalysisMessage]:
885
+ collector = MessageCollector()
886
+ for checkable in checkables:
887
+ collector.extend(checkable.analyze())
888
+ return collector.get()
889
+
890
+
891
+ def analyze_any(
892
+ entity: Union[types.ModuleType, type, FunctionInfo], options: AnalysisOptionSet
893
+ ) -> Iterable[Checkable]:
894
+ if inspect.isclass(entity):
895
+ yield from analyze_class(cast(Type, entity), options)
896
+ elif isinstance(entity, FunctionInfo):
897
+ yield from analyze_function(entity, options)
898
+ elif inspect.ismodule(entity):
899
+ yield from analyze_module(cast(types.ModuleType, entity), options)
900
+ else:
901
+ raise CrossHairInternal("Entity type not analyzable: " + str(type(entity)))
902
+
903
+
904
+ def analyze_module(
905
+ module: types.ModuleType, options: AnalysisOptionSet
906
+ ) -> Iterable[Checkable]:
907
+ """Analyze the classes and functions defined in a module."""
908
+ for name, member in get_top_level_classes_and_functions(module):
909
+ if isinstance(member, type):
910
+ yield from analyze_class(member, options)
911
+ else:
912
+ yield from analyze_function(member, options)
913
+
914
+
915
+ def analyze_class(
916
+ cls: type, options: AnalysisOptionSet = AnalysisOptionSet()
917
+ ) -> Iterable[Checkable]:
918
+ debug("Analyzing class ", cls.__name__)
919
+ analysis_kinds = DEFAULT_OPTIONS.overlay(options).analysis_kind
920
+ with condition_parser(analysis_kinds) as parser:
921
+ class_conditions = parser.get_class_conditions(cls)
922
+ for method_name, conditions in class_conditions.methods.items():
923
+ if method_name == "__init__":
924
+ # Don't check invariants on __init__.
925
+ # (too often this just requires turning the invariant into a very
926
+ # similar precondition)
927
+ filtered_post = [
928
+ c
929
+ for c in conditions.post
930
+ if c.condition_type != ConditionExprType.INVARIANT
931
+ ]
932
+ conditions = replace(conditions, post=filtered_post)
933
+ if conditions.has_any():
934
+ # Note the use of getattr_static to check superclass contracts on
935
+ # functions that the subclass doesn't define.
936
+ ctxfn = FunctionInfo(
937
+ cls, method_name, inspect.getattr_static(cls, method_name)
938
+ )
939
+ for checkable in analyze_function(ctxfn, options=options):
940
+ yield ClampedCheckable(checkable, cls)
941
+
942
+
943
+ def analyze_function(
944
+ ctxfn: Union[FunctionInfo, types.FunctionType, Callable],
945
+ options: AnalysisOptionSet = AnalysisOptionSet(),
946
+ ) -> List[Checkable]:
947
+
948
+ if not isinstance(ctxfn, FunctionInfo):
949
+ ctxfn = FunctionInfo.from_fn(ctxfn)
950
+ debug("Analyzing ", ctxfn.name)
951
+ pair = ctxfn.get_callable()
952
+ fn_options = collect_options(pair[0]) if pair else AnalysisOptionSet()
953
+ full_options = DEFAULT_OPTIONS.overlay(fn_options).overlay(options)
954
+ if not full_options.enabled:
955
+ debug("Skipping", ctxfn.name, " because CrossHair is not enabled")
956
+ return []
957
+
958
+ with condition_parser(full_options.analysis_kind) as parser:
959
+ if not isinstance(ctxfn.context, type):
960
+ conditions = parser.get_fn_conditions(ctxfn)
961
+ else:
962
+ class_conditions = parser.get_class_conditions(ctxfn.context)
963
+ conditions = class_conditions.methods.get(ctxfn.name)
964
+
965
+ if conditions is None:
966
+ debug("Skipping", ctxfn.name, " because it has no conditions")
967
+ return []
968
+ syntax_messages = list(conditions.syntax_messages())
969
+ if syntax_messages:
970
+ debug("Syntax error(s): ", *(m.message for m in syntax_messages))
971
+ messages = [
972
+ AnalysisMessage(
973
+ MessageType.SYNTAX_ERR,
974
+ syntax_message.message,
975
+ syntax_message.filename,
976
+ syntax_message.line_num,
977
+ 0,
978
+ "",
979
+ )
980
+ for syntax_message in syntax_messages
981
+ ]
982
+ return [SyntaxErrorCheckable(messages)]
983
+ return [
984
+ ConditionCheckable(
985
+ ctxfn, full_options, replace(conditions, post=[post_condition])
986
+ )
987
+ for post_condition in conditions.post
988
+ if post_condition.evaluate is not None
989
+ ]
990
+
991
+
992
+ def fn_returning(values: list) -> Callable:
993
+ itr = iter(values)
994
+
995
+ def patched_call(*a, **kw):
996
+ try:
997
+ return next(itr)
998
+ except StopIteration:
999
+ raise NotDeterministic
1000
+
1001
+ return patched_call
1002
+
1003
+
1004
+ class patch_to_return:
1005
+ def __init__(self, return_values: Dict[Callable, list]):
1006
+ self.patches = PatchingModule(
1007
+ {fn: fn_returning(values) for (fn, values) in return_values.items()}
1008
+ )
1009
+
1010
+ def __enter__(self):
1011
+ COMPOSITE_TRACER.push_module(self.patches)
1012
+ return COMPOSITE_TRACER.__enter__()
1013
+
1014
+ def __exit__(self, *a):
1015
+ ret = COMPOSITE_TRACER.__exit__(*a)
1016
+ COMPOSITE_TRACER.pop_config(self.patches)
1017
+ return ret
1018
+
1019
+
1020
+ class FunctionInterps:
1021
+ _interpretations: Dict[Callable, List[object]]
1022
+
1023
+ def __init__(self, *a):
1024
+ self._interpretations = defaultdict(list)
1025
+
1026
+ def append_return(self, callable: Callable, retval: object) -> None:
1027
+ self._interpretations[callable].append(retval)
1028
+
1029
+ def patch_string(self) -> Optional[str]:
1030
+ if self._interpretations:
1031
+ patches = ",".join(
1032
+ f"{method_identifier(fn)}: {eval_friendly_repr(deep_realize(vals))}"
1033
+ for fn, vals in self._interpretations.items()
1034
+ )
1035
+ return f"crosshair.patch_to_return({{{patches}}})"
1036
+ return None
1037
+
1038
+
1039
+ class ShortCircuitingContext:
1040
+ def __init__(self):
1041
+ self.engaged = False
1042
+
1043
+ # Note: this cache is not really for performance; it preserves
1044
+ # function identity so that contract enforcement can correctly detect
1045
+ # re-entrant contracts.
1046
+ self.interceptor_cache = {}
1047
+
1048
+ def __enter__(self):
1049
+ assert not self.engaged
1050
+ self.engaged = True
1051
+
1052
+ def __exit__(self, exc_type, exc_value, tb):
1053
+ assert self.engaged
1054
+ self.engaged = False
1055
+ return False
1056
+
1057
+ def make_interceptor(self, original: Callable) -> Callable:
1058
+ interceptor = self.interceptor_cache.get(original)
1059
+ if interceptor:
1060
+ return interceptor
1061
+
1062
+ # TODO: calling from_fn is wrong here
1063
+ subconditions = get_current_parser().get_fn_conditions(
1064
+ FunctionInfo.from_fn(original)
1065
+ )
1066
+ original_name = original.__name__
1067
+ if subconditions is None:
1068
+ self.interceptor_cache[original] = original
1069
+ return original
1070
+ sig = subconditions.sig
1071
+
1072
+ def _crosshair_wrapper(*a: object, **kw: Dict[str, object]) -> object:
1073
+ space = optional_context_statespace()
1074
+ if (not self.engaged) or (not space):
1075
+ debug("Not short-circuiting", original_name, "(not engaged)")
1076
+ return original(*a, **kw)
1077
+
1078
+ with NoTracing():
1079
+ assert subconditions is not None
1080
+ # Skip function body if it has the option `specs_complete`.
1081
+ short_circuit = collect_options(original).specs_complete
1082
+ # Also skip if the function was manually registered to be skipped.
1083
+ contract = get_contract(original)
1084
+ if contract and contract.skip_body:
1085
+ short_circuit = True
1086
+ # TODO: In the future, sig should be a list of sigs and the parser
1087
+ # would directly return contract.sigs, so no need to fetch it here.
1088
+ sigs = [sig]
1089
+ if contract and contract.sigs:
1090
+ sigs = contract.sigs
1091
+ best_sig = sigs[0]
1092
+ # The function is overloaded, find the best signature.
1093
+ if len(sigs) > 1:
1094
+ new_sig = find_best_sig(sigs, *a, *kw)
1095
+ if new_sig:
1096
+ best_sig = new_sig
1097
+ else:
1098
+ # If no signature is valid, we cannot shortcircuit.
1099
+ short_circuit = False
1100
+ warn(
1101
+ "No signature match with the given parameters for function",
1102
+ original_name,
1103
+ )
1104
+ bound = best_sig.bind(*a, **kw)
1105
+ return_type = consider_shortcircuit(
1106
+ original,
1107
+ best_sig,
1108
+ bound,
1109
+ subconditions,
1110
+ allow_interpretation=not short_circuit,
1111
+ )
1112
+ if short_circuit:
1113
+ assert return_type is not None
1114
+ retval = proxy_for_type(return_type, "proxyreturn" + space.uniq())
1115
+ space.extra(FunctionInterps).append_return(original, retval)
1116
+ debug("short circuit: specs complete; skipping (as uninterpreted)")
1117
+ return retval
1118
+ if return_type is not None:
1119
+ try:
1120
+ self.engaged = False
1121
+ debug(
1122
+ "short circuit: Short circuiting over a call to ", original_name
1123
+ )
1124
+ return shortcircuit(original, best_sig, bound, return_type)
1125
+ finally:
1126
+ self.engaged = True
1127
+ else:
1128
+ debug("short circuit: Not short circuiting", original_name)
1129
+ return original(*a, **kw)
1130
+
1131
+ functools.update_wrapper(_crosshair_wrapper, original)
1132
+ self.interceptor_cache[original] = _crosshair_wrapper
1133
+ return _crosshair_wrapper
1134
+
1135
+
1136
+ @dataclass
1137
+ class CallTreeAnalysis:
1138
+ messages: Sequence[AnalysisMessage]
1139
+ verification_status: VerificationStatus
1140
+ num_confirmed_paths: int = 0
1141
+
1142
+
1143
+ class MessageGenerator:
1144
+ def __init__(self, fn: Callable):
1145
+ self.filename = ""
1146
+ self.start_lineno = 0
1147
+ if hasattr(fn, "__code__"):
1148
+ code_obj = fn.__code__
1149
+ self.filename = code_obj.co_filename
1150
+ self.start_lineno = code_obj.co_firstlineno
1151
+ _, _, lines = sourcelines(fn)
1152
+ self.end_lineno = self.start_lineno + len(lines)
1153
+
1154
+ def make(
1155
+ self,
1156
+ message_type: MessageType,
1157
+ detail: str,
1158
+ suggested_filename: Optional[str],
1159
+ suggested_lineno: int,
1160
+ tb: str,
1161
+ ) -> AnalysisMessage:
1162
+ if (
1163
+ suggested_filename is not None
1164
+ and (os.path.abspath(suggested_filename) == os.path.abspath(self.filename))
1165
+ and (self.start_lineno <= suggested_lineno <= self.end_lineno)
1166
+ ):
1167
+ return AnalysisMessage(
1168
+ message_type, detail, suggested_filename, suggested_lineno, 0, tb
1169
+ )
1170
+ else:
1171
+ exprline = "<unknown>"
1172
+ if suggested_filename is not None:
1173
+ lines = linecache.getlines(suggested_filename)
1174
+ try:
1175
+ exprline = lines[suggested_lineno - 1].strip()
1176
+ except IndexError:
1177
+ pass
1178
+ detail = f'"{exprline}" yields {detail}'
1179
+ return AnalysisMessage(
1180
+ message_type, detail, self.filename, self.start_lineno, 0, tb
1181
+ )
1182
+
1183
+
1184
+ def analyze_calltree(
1185
+ options: AnalysisOptions, conditions: Conditions
1186
+ ) -> CallTreeAnalysis:
1187
+ fn = conditions.fn
1188
+ debug("Begin analyze calltree ", fn.__name__)
1189
+
1190
+ all_messages = MessageCollector()
1191
+ search_root = RootNode()
1192
+ space_exhausted = False
1193
+ failing_precondition: Optional[ConditionExpr] = (
1194
+ conditions.pre[0] if conditions.pre else None
1195
+ )
1196
+ failing_precondition_reason: str = ""
1197
+ num_confirmed_paths = 0
1198
+
1199
+ short_circuit = ShortCircuitingContext()
1200
+ top_analysis: Optional[CallAnalysis] = None
1201
+ enforced_conditions = EnforcedConditions(
1202
+ interceptor=short_circuit.make_interceptor,
1203
+ )
1204
+ max_uninteresting_iterations = options.get_max_uninteresting_iterations()
1205
+ patched = Patched()
1206
+ # TODO clean up how encofrced conditions works here?
1207
+ with patched:
1208
+ for i in range(1, options.max_iterations + 1):
1209
+ start = monotonic()
1210
+ if start > options.deadline:
1211
+ debug("Exceeded condition timeout, stopping")
1212
+ break
1213
+ options.incr("num_paths")
1214
+ debug("Iteration ", i)
1215
+ per_path_timeout = options.get_per_path_timeout()
1216
+ space = StateSpace(
1217
+ execution_deadline=start + per_path_timeout,
1218
+ model_check_timeout=per_path_timeout / 2,
1219
+ search_root=search_root,
1220
+ )
1221
+ try:
1222
+ with StateSpaceContext(space), COMPOSITE_TRACER, NoTracing():
1223
+ # The real work happens here!:
1224
+ call_analysis = attempt_call(
1225
+ conditions, short_circuit, enforced_conditions
1226
+ )
1227
+ if failing_precondition is not None:
1228
+ cur_precondition = call_analysis.failing_precondition
1229
+ if cur_precondition is None:
1230
+ if call_analysis.verification_status is not None:
1231
+ # We escaped the all the pre conditions on this try:
1232
+ failing_precondition = None
1233
+ elif (
1234
+ cur_precondition.line == failing_precondition.line
1235
+ and call_analysis.failing_precondition_reason
1236
+ ):
1237
+ failing_precondition_reason = (
1238
+ call_analysis.failing_precondition_reason
1239
+ )
1240
+ elif cur_precondition.line > failing_precondition.line:
1241
+ failing_precondition = cur_precondition
1242
+ failing_precondition_reason = (
1243
+ call_analysis.failing_precondition_reason
1244
+ )
1245
+
1246
+ except NotDeterministic:
1247
+ # TODO: Improve nondeterminism helpfulness
1248
+ tb = extract_tb(sys.exc_info()[2])
1249
+ frame_filename, frame_lineno = frame_summary_for_fn(
1250
+ conditions.src_fn, tb
1251
+ )
1252
+ msg_gen = MessageGenerator(conditions.src_fn)
1253
+ call_analysis = CallAnalysis(
1254
+ VerificationStatus.REFUTED,
1255
+ [
1256
+ msg_gen.make(
1257
+ MessageType.EXEC_ERR,
1258
+ "NotDeterministic: Found a different execution paths after making the same decisions",
1259
+ frame_filename,
1260
+ frame_lineno,
1261
+ traceback.format_exc(),
1262
+ )
1263
+ ],
1264
+ )
1265
+ except UnexploredPath:
1266
+ call_analysis = CallAnalysis(VerificationStatus.UNKNOWN)
1267
+ except IgnoreAttempt:
1268
+ call_analysis = CallAnalysis()
1269
+ status = call_analysis.verification_status
1270
+ if status == VerificationStatus.CONFIRMED:
1271
+ num_confirmed_paths += 1
1272
+ top_analysis, space_exhausted = space.bubble_status(call_analysis)
1273
+ debug("Path tree stats", search_root.stats())
1274
+ overall_status = top_analysis.verification_status if top_analysis else None
1275
+ debug(
1276
+ "Iter complete. Worst status found so far:",
1277
+ overall_status.name if overall_status else "None",
1278
+ )
1279
+ iters_since_discovery = getattr(
1280
+ search_root.pathing_oracle, "iters_since_discovery"
1281
+ )
1282
+ assert isinstance(iters_since_discovery, int)
1283
+ if iters_since_discovery > max_uninteresting_iterations:
1284
+ break
1285
+ if space_exhausted or overall_status == VerificationStatus.REFUTED:
1286
+ break
1287
+ top_analysis = search_root.child.get_result()
1288
+ if top_analysis.messages:
1289
+ all_messages.extend(
1290
+ replace(
1291
+ m, test_fn=fn.__qualname__, condition_src=conditions.post[0].expr_source
1292
+ )
1293
+ for m in top_analysis.messages
1294
+ )
1295
+ if top_analysis.verification_status is None:
1296
+ top_analysis.verification_status = VerificationStatus.UNKNOWN
1297
+ if failing_precondition:
1298
+ assert num_confirmed_paths == 0
1299
+ message = f"Unable to meet precondition"
1300
+ if failing_precondition_reason:
1301
+ message += f" (possibly because {failing_precondition_reason}?)"
1302
+ all_messages.extend(
1303
+ [
1304
+ AnalysisMessage(
1305
+ MessageType.PRE_UNSAT,
1306
+ message + ".",
1307
+ failing_precondition.filename,
1308
+ failing_precondition.line,
1309
+ 0,
1310
+ "",
1311
+ )
1312
+ ]
1313
+ )
1314
+ top_analysis = CallAnalysis(VerificationStatus.REFUTED)
1315
+
1316
+ assert top_analysis.verification_status is not None
1317
+ debug(
1318
+ ("Exhausted" if space_exhausted else "Aborted"),
1319
+ "calltree search with",
1320
+ top_analysis.verification_status.name,
1321
+ "and",
1322
+ len(all_messages.get()),
1323
+ "messages.",
1324
+ "Number of iterations: ",
1325
+ i - 1,
1326
+ )
1327
+ return CallTreeAnalysis(
1328
+ messages=all_messages.get(),
1329
+ verification_status=top_analysis.verification_status,
1330
+ num_confirmed_paths=num_confirmed_paths,
1331
+ )
1332
+
1333
+
1334
+ PathCompeltionCallback = Callable[
1335
+ [
1336
+ StateSpace,
1337
+ BoundArguments,
1338
+ BoundArguments,
1339
+ Any,
1340
+ Optional[BaseException],
1341
+ Optional[StackSummary],
1342
+ ],
1343
+ bool,
1344
+ ]
1345
+
1346
+
1347
+ def explore_paths(
1348
+ fn: Callable[[BoundArguments], Any],
1349
+ sig: Signature,
1350
+ options: AnalysisOptions,
1351
+ search_root: RootNode,
1352
+ on_path_complete: PathCompeltionCallback = (lambda *a: False),
1353
+ ) -> None:
1354
+ """
1355
+ Runs a path exploration for use cases beyond invariant checking.
1356
+ """
1357
+ condition_start = monotonic()
1358
+ breakout = False
1359
+ max_uninteresting_iterations = options.get_max_uninteresting_iterations()
1360
+ for i in range(1, options.max_iterations + 1):
1361
+ debug("Iteration ", i)
1362
+ itr_start = monotonic()
1363
+ if itr_start > condition_start + options.per_condition_timeout:
1364
+ debug(
1365
+ "Stopping due to --per_condition_timeout=",
1366
+ options.per_condition_timeout,
1367
+ )
1368
+ break
1369
+ per_path_timeout = options.get_per_path_timeout()
1370
+ space = StateSpace(
1371
+ execution_deadline=itr_start + per_path_timeout,
1372
+ model_check_timeout=per_path_timeout / 2,
1373
+ search_root=search_root,
1374
+ )
1375
+ with condition_parser(
1376
+ options.analysis_kind
1377
+ ), Patched(), COMPOSITE_TRACER, NoTracing(), StateSpaceContext(space):
1378
+ try:
1379
+ pre_args = gen_args(sig)
1380
+ args = deepcopyext(pre_args, CopyMode.REGULAR, {})
1381
+ ret: object = None
1382
+ user_exc: Optional[BaseException] = None
1383
+ user_exc_stack: Optional[StackSummary] = None
1384
+ with ExceptionFilter() as efilter, ResumedTracing():
1385
+ ret = fn(args)
1386
+ if efilter.user_exc:
1387
+ if isinstance(efilter.user_exc[0], NotDeterministic):
1388
+ raise NotDeterministic
1389
+ else:
1390
+ user_exc, user_exc_stack = efilter.user_exc
1391
+ with ResumedTracing():
1392
+ breakout = on_path_complete(
1393
+ space, pre_args, args, ret, user_exc, user_exc_stack
1394
+ )
1395
+ verification_status = VerificationStatus.CONFIRMED
1396
+ except IgnoreAttempt:
1397
+ verification_status = None
1398
+ except UnexploredPath:
1399
+ verification_status = VerificationStatus.UNKNOWN
1400
+ debug("Verification status:", verification_status)
1401
+ _analysis, exhausted = space.bubble_status(
1402
+ CallAnalysis(verification_status)
1403
+ )
1404
+ debug("Path tree stats", search_root.stats())
1405
+ if breakout:
1406
+ break
1407
+ if exhausted:
1408
+ debug("Stopping due to path exhaustion")
1409
+ break
1410
+ if max_uninteresting_iterations != sys.maxsize:
1411
+ iters_since_discovery = getattr(
1412
+ search_root.pathing_oracle, "iters_since_discovery"
1413
+ )
1414
+ assert isinstance(iters_since_discovery, int)
1415
+ debug("iters_since_discovery", iters_since_discovery)
1416
+ if iters_since_discovery > max_uninteresting_iterations:
1417
+ debug(
1418
+ "Stopping due to --max_uninteresting_iterations=",
1419
+ max_uninteresting_iterations,
1420
+ )
1421
+ break
1422
+
1423
+
1424
+ def make_counterexample_message(
1425
+ conditions: Conditions, args: BoundArguments, return_val: object = None
1426
+ ) -> str:
1427
+ reprer = context_statespace().extra(LazyCreationRepr)
1428
+
1429
+ with NoTracing():
1430
+ args = reprer.deep_realize(args)
1431
+
1432
+ return_val = deep_realize(return_val)
1433
+
1434
+ with NoTracing():
1435
+ invocation, retstring = conditions.format_counterexample(
1436
+ args, return_val, reprer.reprs
1437
+ )
1438
+
1439
+ patch_expr = context_statespace().extra(FunctionInterps).patch_string()
1440
+ if patch_expr:
1441
+ invocation += f" with {patch_expr}"
1442
+ if retstring == "None":
1443
+ return f"when calling {invocation}"
1444
+ else:
1445
+ return f"when calling {invocation} (which returns {retstring})"
1446
+
1447
+
1448
+ def attempt_call(
1449
+ conditions: Conditions,
1450
+ short_circuit: ShortCircuitingContext,
1451
+ enforced_conditions: EnforcedConditions,
1452
+ ) -> CallAnalysis:
1453
+ assert not is_tracing()
1454
+ fn = conditions.fn
1455
+ space = context_statespace()
1456
+ msg_gen = MessageGenerator(conditions.src_fn)
1457
+ with enforced_conditions.enabled_enforcement():
1458
+ original_args = gen_args(conditions.sig)
1459
+ space.checkpoint()
1460
+ bound_args = deepcopyext(original_args, CopyMode.BEST_EFFORT, {})
1461
+
1462
+ lcls: Mapping[str, object] = bound_args.arguments
1463
+ # In preconditions, __old__ exists but is just bound to the same args.
1464
+ # This lets people write class invariants using `__old__` to, for example,
1465
+ # demonstrate immutability.
1466
+ lcls = {"__old__": AttributeHolder(lcls), **lcls}
1467
+ expected_exceptions = conditions.raises
1468
+ for precondition in conditions.pre:
1469
+ if not precondition.evaluate:
1470
+ continue
1471
+ with ExceptionFilter(expected_exceptions) as efilter:
1472
+ with enforced_conditions.enabled_enforcement(), short_circuit:
1473
+ with ResumedTracing():
1474
+ precondition_ok = precondition.evaluate(lcls)
1475
+ precondition_ok = realize(prefer_true(precondition_ok))
1476
+ if not precondition_ok:
1477
+ debug("Failed to meet precondition", precondition.expr_source)
1478
+ return CallAnalysis(failing_precondition=precondition)
1479
+ if efilter.ignore:
1480
+ debug("Ignored exception in precondition.", efilter.analysis)
1481
+ return efilter.analysis
1482
+ elif efilter.user_exc is not None:
1483
+ (user_exc, tb) = efilter.user_exc
1484
+ formatted_tb = tb.format()
1485
+ debug(
1486
+ "Exception attempting to meet precondition",
1487
+ precondition.expr_source,
1488
+ ":",
1489
+ user_exc,
1490
+ formatted_tb,
1491
+ )
1492
+ return CallAnalysis(
1493
+ failing_precondition=precondition,
1494
+ failing_precondition_reason=f'it raised "{repr(user_exc)} at {formatted_tb[-1]}"',
1495
+ )
1496
+
1497
+ with ExceptionFilter(expected_exceptions) as efilter:
1498
+ unenforced_fn = NoEnforce(fn)
1499
+ bargs, bkwargs = bound_args.args, bound_args.kwargs
1500
+ debug("Starting function body")
1501
+ with enforced_conditions.enabled_enforcement(), short_circuit, ResumedTracing():
1502
+ __return__ = unenforced_fn(*bargs, **bkwargs)
1503
+ lcls = {
1504
+ **bound_args.arguments,
1505
+ "__return__": __return__,
1506
+ "_": __return__,
1507
+ "__old__": AttributeHolder(original_args.arguments),
1508
+ fn.__name__: fn,
1509
+ }
1510
+
1511
+ if efilter.ignore:
1512
+ debug("Ignored exception in function.", efilter.analysis)
1513
+ return efilter.analysis
1514
+ elif efilter.user_exc is not None:
1515
+ (e, tb) = efilter.user_exc
1516
+ detail = name_of_type(type(e)) + ": " + str(e)
1517
+ tb_desc = tb.format()
1518
+ frame_filename, frame_lineno = frame_summary_for_fn(conditions.src_fn, tb)
1519
+ with ResumedTracing():
1520
+ space.detach_path(e)
1521
+ detail += " " + make_counterexample_message(conditions, original_args)
1522
+ debug("exception while evaluating function body:", detail)
1523
+ debug("exception traceback:", ch_stack(tb))
1524
+ return CallAnalysis(
1525
+ VerificationStatus.REFUTED,
1526
+ [
1527
+ msg_gen.make(
1528
+ MessageType.EXEC_ERR,
1529
+ detail,
1530
+ frame_filename,
1531
+ frame_lineno,
1532
+ "".join(tb_desc),
1533
+ )
1534
+ ],
1535
+ )
1536
+
1537
+ for argname, argval in bound_args.arguments.items():
1538
+ if (
1539
+ conditions.mutable_args is not None
1540
+ and argname not in conditions.mutable_args
1541
+ ):
1542
+ old_val, new_val = original_args.arguments[argname], argval
1543
+ with ResumedTracing():
1544
+ if old_val != new_val:
1545
+ space.detach_path()
1546
+ detail = 'Argument "{}" is not marked as mutable, but changed from {} to {}'.format(
1547
+ argname, old_val, new_val
1548
+ )
1549
+ debug("Mutablity problem:", detail)
1550
+ return CallAnalysis(
1551
+ VerificationStatus.REFUTED,
1552
+ [msg_gen.make(MessageType.POST_ERR, detail, None, 0, "")],
1553
+ )
1554
+
1555
+ (post_condition,) = conditions.post
1556
+ assert post_condition.evaluate is not None
1557
+ with ExceptionFilter(expected_exceptions) as efilter:
1558
+ # TODO: re-enable post-condition short circuiting. This will require refactoring how
1559
+ # enforced conditions and short curcuiting interact, so that post-conditions are
1560
+ # selectively run when, and only when, performing a short circuit.
1561
+ # with enforced_conditions.enabled_enforcement(), short_circuit:
1562
+ debug("Starting postcondition")
1563
+ with ResumedTracing():
1564
+ isok = bool(post_condition.evaluate(lcls))
1565
+ if efilter.ignore:
1566
+ debug("Ignored exception in postcondition.", efilter.analysis)
1567
+ return efilter.analysis
1568
+ elif efilter.user_exc is not None:
1569
+ (e, tb) = efilter.user_exc
1570
+ detail = name_of_type(type(e)) + ": " + str(e)
1571
+ with ResumedTracing():
1572
+ space.detach_path(e)
1573
+ detail += " " + make_counterexample_message(
1574
+ conditions, original_args, __return__
1575
+ )
1576
+ debug("exception while calling postcondition:", detail)
1577
+ debug("exception traceback:", ch_stack(tb))
1578
+ failures = [
1579
+ msg_gen.make(
1580
+ MessageType.POST_ERR,
1581
+ detail,
1582
+ post_condition.filename,
1583
+ post_condition.line,
1584
+ "".join(tb.format()),
1585
+ )
1586
+ ]
1587
+ return CallAnalysis(VerificationStatus.REFUTED, failures)
1588
+ if isok:
1589
+ debug("Postcondition confirmed.")
1590
+ return CallAnalysis(VerificationStatus.CONFIRMED)
1591
+ else:
1592
+ with ResumedTracing():
1593
+ space.detach_path()
1594
+ detail = "false " + make_counterexample_message(
1595
+ conditions, original_args, __return__
1596
+ )
1597
+ debug(detail)
1598
+ failures = [
1599
+ msg_gen.make(
1600
+ MessageType.POST_FAIL,
1601
+ detail,
1602
+ post_condition.filename,
1603
+ post_condition.line,
1604
+ "",
1605
+ )
1606
+ ]
1607
+ return CallAnalysis(VerificationStatus.REFUTED, failures)
1608
+
1609
+
1610
+ def _mutability_testing_hash(o: object) -> int:
1611
+ if isinstance(o, ATOMIC_IMMUTABLE_TYPES):
1612
+ return 0
1613
+ if hasattr(o, "__ch_is_deeply_immutable__"):
1614
+ if o.__ch_is_deeply_immutable__(): # type: ignore
1615
+ return 0
1616
+ else:
1617
+ raise TypeError
1618
+ typ = type(o)
1619
+ if not hasattr(typ, "__hash__"): # TODO: test for __hash__ = None (list has this)
1620
+ raise TypeError
1621
+ # We err on the side of mutability if this object is using the default hash:
1622
+ if typ.__hash__ is object.__hash__:
1623
+ raise TypeError
1624
+ return typ.__hash__(o)
1625
+
1626
+
1627
+ def is_deeply_immutable(o: object) -> bool:
1628
+ if not is_tracing():
1629
+ raise CrossHairInternal("is_deeply_immutable must be run with tracing enabled")
1630
+ orig_modules = COMPOSITE_TRACER.get_modules()
1631
+ hash_intercept_module = PatchingModule({hash: _mutability_testing_hash})
1632
+ for module in reversed(orig_modules):
1633
+ COMPOSITE_TRACER.pop_config(module)
1634
+ COMPOSITE_TRACER.push_module(hash_intercept_module)
1635
+ try:
1636
+ try:
1637
+ hash(o)
1638
+ return True
1639
+ except TypeError:
1640
+ return False
1641
+ finally:
1642
+ COMPOSITE_TRACER.pop_config(hash_intercept_module)
1643
+ for module in orig_modules:
1644
+ COMPOSITE_TRACER.push_module(module)
1645
+
1646
+
1647
+ def find_best_sig(
1648
+ sigs: List[Signature],
1649
+ *args: object,
1650
+ **kwargs: Dict[str, object],
1651
+ ) -> Optional[Signature]:
1652
+ """Return the first signature which complies with the args."""
1653
+ for sig in sigs:
1654
+ bound = sig.bind(*args, **kwargs)
1655
+ bound.apply_defaults()
1656
+ bindings: typing.ChainMap[object, type] = ChainMap()
1657
+ is_valid = True
1658
+ for param in sig.parameters.values():
1659
+ argval = bound.arguments[param.name]
1660
+ value_type = python_type(argval)
1661
+ if not dynamic_typing.unify(value_type, param.annotation, bindings):
1662
+ is_valid = False
1663
+ break
1664
+ if is_valid:
1665
+ return sig
1666
+ return None
1667
+
1668
+
1669
+ def consider_shortcircuit(
1670
+ fn: Callable,
1671
+ sig: Signature,
1672
+ bound: BoundArguments,
1673
+ subconditions: Conditions,
1674
+ allow_interpretation: bool,
1675
+ ) -> Optional[type]:
1676
+ """
1677
+ Consider the feasibility of short-circuiting (skipping) a function with the given arguments.
1678
+
1679
+ :return: The type of a symbolic value that could be returned by ``fn``.
1680
+ :return: None if a short-circuiting should not be attempted.
1681
+ """
1682
+ return_type = sig.return_annotation
1683
+ if return_type == Signature.empty:
1684
+ return_type = object
1685
+ elif return_type is None:
1686
+ return_type = type(None)
1687
+
1688
+ mutable_args = subconditions.mutable_args
1689
+ if allow_interpretation:
1690
+ if mutable_args is None or len(mutable_args) > 0:
1691
+ # we don't deal with mutation inside the skipped function yet.
1692
+ debug("aborting shortcircuit: function has matuable args")
1693
+ return None
1694
+
1695
+ # Deduce type vars if necessary
1696
+ if len(typing_inspect.get_parameters(return_type)) > 0 or typing_inspect.is_typevar(
1697
+ return_type
1698
+ ):
1699
+
1700
+ typevar_bindings: typing.ChainMap[object, type] = ChainMap()
1701
+ bound.apply_defaults()
1702
+ for param in sig.parameters.values():
1703
+ argval = bound.arguments[param.name]
1704
+ # We don't need all args to be symbolic, but we don't currently
1705
+ # short circuit in that case as a heuristic.
1706
+ if allow_interpretation and not isinstance(argval, CrossHairValue):
1707
+ debug("aborting shortcircuit:", param.name, "is not symbolic")
1708
+ return None
1709
+ value_type = python_type(argval)
1710
+ if not dynamic_typing.unify(value_type, param.annotation, typevar_bindings):
1711
+ if allow_interpretation:
1712
+ debug("aborting shortcircuit", param.name, "fails unification")
1713
+ return None
1714
+ else:
1715
+ raise CrosshairUnsupported
1716
+ return_type = dynamic_typing.realize(sig.return_annotation, typevar_bindings)
1717
+
1718
+ if not allow_interpretation:
1719
+ return return_type
1720
+
1721
+ space = context_statespace()
1722
+ short_stats, callinto_stats = space.stats_lookahead()
1723
+ if callinto_stats.unknown_pct < short_stats.unknown_pct:
1724
+ callinto_probability = 1.0
1725
+ else:
1726
+ callinto_probability = 0.7
1727
+
1728
+ debug("short circuit: call-into probability", callinto_probability)
1729
+ do_short_circuit = space.fork_parallel(
1730
+ callinto_probability, desc=f"shortcircuit {fn.__name__}"
1731
+ )
1732
+ return return_type if do_short_circuit else None
1733
+
1734
+
1735
+ def shortcircuit(
1736
+ fn: Callable, sig: Signature, bound: BoundArguments, return_type: Type
1737
+ ) -> object:
1738
+ space = context_statespace()
1739
+ debug("short circuit: Deduced return type was ", return_type)
1740
+
1741
+ # Deep copy the arguments for reconciliation later.
1742
+ # (we know that this function won't mutate them, but not that others won't)
1743
+ argscopy = {}
1744
+ for name, val in bound.arguments.items():
1745
+ if is_deeply_immutable(val):
1746
+ argscopy[name] = val
1747
+ else:
1748
+ with NoTracing():
1749
+ argscopy[name] = deepcopyext(val, CopyMode.BEST_EFFORT, {})
1750
+ bound_copy = BoundArguments(sig, argscopy) # type: ignore
1751
+
1752
+ retval = None
1753
+ if return_type is not type(None):
1754
+ # note that the enforcement wrapper ensures postconditions for us, so
1755
+ # we can just return a free variable here.
1756
+ retval = proxy_for_type(return_type, "proxyreturn" + space.uniq())
1757
+
1758
+ def reconciled() -> bool:
1759
+ return retval == fn(*bound_copy.args, **bound_copy.kwargs)
1760
+
1761
+ space.defer_assumption("Reconcile short circuit", reconciled)
1762
+
1763
+ return retval