crosshair-tool 0.0.99__cp312-cp312-macosx_10_13_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- _crosshair_tracers.cpython-312-darwin.so +0 -0
- crosshair/__init__.py +42 -0
- crosshair/__main__.py +8 -0
- crosshair/_mark_stacks.h +790 -0
- crosshair/_preliminaries_test.py +18 -0
- crosshair/_tracers.h +94 -0
- crosshair/_tracers_pycompat.h +522 -0
- crosshair/_tracers_test.py +138 -0
- crosshair/abcstring.py +245 -0
- crosshair/auditwall.py +190 -0
- crosshair/auditwall_test.py +77 -0
- crosshair/codeconfig.py +113 -0
- crosshair/codeconfig_test.py +117 -0
- crosshair/condition_parser.py +1237 -0
- crosshair/condition_parser_test.py +497 -0
- crosshair/conftest.py +30 -0
- crosshair/copyext.py +155 -0
- crosshair/copyext_test.py +84 -0
- crosshair/core.py +1763 -0
- crosshair/core_and_libs.py +149 -0
- crosshair/core_regestered_types_test.py +82 -0
- crosshair/core_test.py +1316 -0
- crosshair/diff_behavior.py +314 -0
- crosshair/diff_behavior_test.py +261 -0
- crosshair/dynamic_typing.py +346 -0
- crosshair/dynamic_typing_test.py +210 -0
- crosshair/enforce.py +282 -0
- crosshair/enforce_test.py +182 -0
- crosshair/examples/PEP316/__init__.py +1 -0
- crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
- crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
- crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
- crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
- crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
- crosshair/examples/PEP316/correct_code/__init__.py +0 -0
- crosshair/examples/PEP316/correct_code/arith.py +60 -0
- crosshair/examples/PEP316/correct_code/chess.py +77 -0
- crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
- crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
- crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
- crosshair/examples/PEP316/correct_code/showcase.py +104 -0
- crosshair/examples/__init__.py +0 -0
- crosshair/examples/check_examples_test.py +146 -0
- crosshair/examples/deal/__init__.py +1 -0
- crosshair/examples/icontract/__init__.py +1 -0
- crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
- crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
- crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
- crosshair/examples/icontract/correct_code/__init__.py +0 -0
- crosshair/examples/icontract/correct_code/arith.py +51 -0
- crosshair/examples/icontract/correct_code/showcase.py +94 -0
- crosshair/fnutil.py +391 -0
- crosshair/fnutil_test.py +75 -0
- crosshair/fuzz_core_test.py +516 -0
- crosshair/libimpl/__init__.py +0 -0
- crosshair/libimpl/arraylib.py +161 -0
- crosshair/libimpl/binascii_ch_test.py +30 -0
- crosshair/libimpl/binascii_test.py +67 -0
- crosshair/libimpl/binasciilib.py +150 -0
- crosshair/libimpl/bisectlib_test.py +23 -0
- crosshair/libimpl/builtinslib.py +5228 -0
- crosshair/libimpl/builtinslib_ch_test.py +1191 -0
- crosshair/libimpl/builtinslib_test.py +3735 -0
- crosshair/libimpl/codecslib.py +86 -0
- crosshair/libimpl/codecslib_test.py +86 -0
- crosshair/libimpl/collectionslib.py +264 -0
- crosshair/libimpl/collectionslib_ch_test.py +252 -0
- crosshair/libimpl/collectionslib_test.py +332 -0
- crosshair/libimpl/copylib.py +23 -0
- crosshair/libimpl/copylib_test.py +18 -0
- crosshair/libimpl/datetimelib.py +2559 -0
- crosshair/libimpl/datetimelib_ch_test.py +354 -0
- crosshair/libimpl/datetimelib_test.py +112 -0
- crosshair/libimpl/decimallib.py +5257 -0
- crosshair/libimpl/decimallib_ch_test.py +78 -0
- crosshair/libimpl/decimallib_test.py +76 -0
- crosshair/libimpl/encodings/__init__.py +23 -0
- crosshair/libimpl/encodings/_encutil.py +187 -0
- crosshair/libimpl/encodings/ascii.py +44 -0
- crosshair/libimpl/encodings/latin_1.py +40 -0
- crosshair/libimpl/encodings/utf_8.py +93 -0
- crosshair/libimpl/encodings_ch_test.py +83 -0
- crosshair/libimpl/fractionlib.py +16 -0
- crosshair/libimpl/fractionlib_test.py +80 -0
- crosshair/libimpl/functoolslib.py +34 -0
- crosshair/libimpl/functoolslib_test.py +56 -0
- crosshair/libimpl/hashliblib.py +30 -0
- crosshair/libimpl/hashliblib_test.py +18 -0
- crosshair/libimpl/heapqlib.py +47 -0
- crosshair/libimpl/heapqlib_test.py +21 -0
- crosshair/libimpl/importliblib.py +18 -0
- crosshair/libimpl/importliblib_test.py +38 -0
- crosshair/libimpl/iolib.py +216 -0
- crosshair/libimpl/iolib_ch_test.py +128 -0
- crosshair/libimpl/iolib_test.py +19 -0
- crosshair/libimpl/ipaddresslib.py +8 -0
- crosshair/libimpl/itertoolslib.py +44 -0
- crosshair/libimpl/itertoolslib_test.py +44 -0
- crosshair/libimpl/jsonlib.py +984 -0
- crosshair/libimpl/jsonlib_ch_test.py +42 -0
- crosshair/libimpl/jsonlib_test.py +51 -0
- crosshair/libimpl/mathlib.py +179 -0
- crosshair/libimpl/mathlib_ch_test.py +44 -0
- crosshair/libimpl/mathlib_test.py +67 -0
- crosshair/libimpl/oslib.py +7 -0
- crosshair/libimpl/pathliblib_test.py +10 -0
- crosshair/libimpl/randomlib.py +178 -0
- crosshair/libimpl/randomlib_test.py +120 -0
- crosshair/libimpl/relib.py +846 -0
- crosshair/libimpl/relib_ch_test.py +169 -0
- crosshair/libimpl/relib_test.py +493 -0
- crosshair/libimpl/timelib.py +72 -0
- crosshair/libimpl/timelib_test.py +82 -0
- crosshair/libimpl/typeslib.py +15 -0
- crosshair/libimpl/typeslib_test.py +36 -0
- crosshair/libimpl/unicodedatalib.py +75 -0
- crosshair/libimpl/unicodedatalib_test.py +42 -0
- crosshair/libimpl/urlliblib.py +23 -0
- crosshair/libimpl/urlliblib_test.py +19 -0
- crosshair/libimpl/weakreflib.py +13 -0
- crosshair/libimpl/weakreflib_test.py +69 -0
- crosshair/libimpl/zliblib.py +15 -0
- crosshair/libimpl/zliblib_test.py +13 -0
- crosshair/lsp_server.py +261 -0
- crosshair/lsp_server_test.py +30 -0
- crosshair/main.py +973 -0
- crosshair/main_test.py +543 -0
- crosshair/objectproxy.py +376 -0
- crosshair/objectproxy_test.py +41 -0
- crosshair/opcode_intercept.py +601 -0
- crosshair/opcode_intercept_test.py +304 -0
- crosshair/options.py +218 -0
- crosshair/options_test.py +10 -0
- crosshair/patch_equivalence_test.py +75 -0
- crosshair/path_cover.py +209 -0
- crosshair/path_cover_test.py +138 -0
- crosshair/path_search.py +161 -0
- crosshair/path_search_test.py +52 -0
- crosshair/pathing_oracle.py +271 -0
- crosshair/pathing_oracle_test.py +21 -0
- crosshair/pure_importer.py +27 -0
- crosshair/pure_importer_test.py +16 -0
- crosshair/py.typed +0 -0
- crosshair/register_contract.py +273 -0
- crosshair/register_contract_test.py +190 -0
- crosshair/simplestructs.py +1165 -0
- crosshair/simplestructs_test.py +283 -0
- crosshair/smtlib.py +24 -0
- crosshair/smtlib_test.py +14 -0
- crosshair/statespace.py +1199 -0
- crosshair/statespace_test.py +108 -0
- crosshair/stubs_parser.py +352 -0
- crosshair/stubs_parser_test.py +43 -0
- crosshair/test_util.py +329 -0
- crosshair/test_util_test.py +26 -0
- crosshair/tools/__init__.py +0 -0
- crosshair/tools/check_help_in_doc.py +264 -0
- crosshair/tools/check_init_and_setup_coincide.py +119 -0
- crosshair/tools/generate_demo_table.py +127 -0
- crosshair/tracers.py +544 -0
- crosshair/tracers_test.py +154 -0
- crosshair/type_repo.py +151 -0
- crosshair/unicode_categories.py +589 -0
- crosshair/unicode_categories_test.py +27 -0
- crosshair/util.py +741 -0
- crosshair/util_test.py +173 -0
- crosshair/watcher.py +307 -0
- crosshair/watcher_test.py +107 -0
- crosshair/z3util.py +76 -0
- crosshair/z3util_test.py +11 -0
- crosshair_tool-0.0.99.dist-info/METADATA +144 -0
- crosshair_tool-0.0.99.dist-info/RECORD +176 -0
- crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
- crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
- crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
- crosshair_tool-0.0.99.dist-info/top_level.txt +2 -0
crosshair/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
|