crosshair-tool 0.0.99__cp312-cp312-macosx_10_13_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. _crosshair_tracers.cpython-312-darwin.so +0 -0
  2. crosshair/__init__.py +42 -0
  3. crosshair/__main__.py +8 -0
  4. crosshair/_mark_stacks.h +790 -0
  5. crosshair/_preliminaries_test.py +18 -0
  6. crosshair/_tracers.h +94 -0
  7. crosshair/_tracers_pycompat.h +522 -0
  8. crosshair/_tracers_test.py +138 -0
  9. crosshair/abcstring.py +245 -0
  10. crosshair/auditwall.py +190 -0
  11. crosshair/auditwall_test.py +77 -0
  12. crosshair/codeconfig.py +113 -0
  13. crosshair/codeconfig_test.py +117 -0
  14. crosshair/condition_parser.py +1237 -0
  15. crosshair/condition_parser_test.py +497 -0
  16. crosshair/conftest.py +30 -0
  17. crosshair/copyext.py +155 -0
  18. crosshair/copyext_test.py +84 -0
  19. crosshair/core.py +1763 -0
  20. crosshair/core_and_libs.py +149 -0
  21. crosshair/core_regestered_types_test.py +82 -0
  22. crosshair/core_test.py +1316 -0
  23. crosshair/diff_behavior.py +314 -0
  24. crosshair/diff_behavior_test.py +261 -0
  25. crosshair/dynamic_typing.py +346 -0
  26. crosshair/dynamic_typing_test.py +210 -0
  27. crosshair/enforce.py +282 -0
  28. crosshair/enforce_test.py +182 -0
  29. crosshair/examples/PEP316/__init__.py +1 -0
  30. crosshair/examples/PEP316/bugs_detected/__init__.py +0 -0
  31. crosshair/examples/PEP316/bugs_detected/getattr_magic.py +16 -0
  32. crosshair/examples/PEP316/bugs_detected/hash_consistent_with_equals.py +31 -0
  33. crosshair/examples/PEP316/bugs_detected/shopping_cart.py +24 -0
  34. crosshair/examples/PEP316/bugs_detected/showcase.py +39 -0
  35. crosshair/examples/PEP316/correct_code/__init__.py +0 -0
  36. crosshair/examples/PEP316/correct_code/arith.py +60 -0
  37. crosshair/examples/PEP316/correct_code/chess.py +77 -0
  38. crosshair/examples/PEP316/correct_code/nesting_inference.py +17 -0
  39. crosshair/examples/PEP316/correct_code/numpy_examples.py +132 -0
  40. crosshair/examples/PEP316/correct_code/rolling_average.py +35 -0
  41. crosshair/examples/PEP316/correct_code/showcase.py +104 -0
  42. crosshair/examples/__init__.py +0 -0
  43. crosshair/examples/check_examples_test.py +146 -0
  44. crosshair/examples/deal/__init__.py +1 -0
  45. crosshair/examples/icontract/__init__.py +1 -0
  46. crosshair/examples/icontract/bugs_detected/__init__.py +0 -0
  47. crosshair/examples/icontract/bugs_detected/showcase.py +41 -0
  48. crosshair/examples/icontract/bugs_detected/wrong_sign.py +8 -0
  49. crosshair/examples/icontract/correct_code/__init__.py +0 -0
  50. crosshair/examples/icontract/correct_code/arith.py +51 -0
  51. crosshair/examples/icontract/correct_code/showcase.py +94 -0
  52. crosshair/fnutil.py +391 -0
  53. crosshair/fnutil_test.py +75 -0
  54. crosshair/fuzz_core_test.py +516 -0
  55. crosshair/libimpl/__init__.py +0 -0
  56. crosshair/libimpl/arraylib.py +161 -0
  57. crosshair/libimpl/binascii_ch_test.py +30 -0
  58. crosshair/libimpl/binascii_test.py +67 -0
  59. crosshair/libimpl/binasciilib.py +150 -0
  60. crosshair/libimpl/bisectlib_test.py +23 -0
  61. crosshair/libimpl/builtinslib.py +5228 -0
  62. crosshair/libimpl/builtinslib_ch_test.py +1191 -0
  63. crosshair/libimpl/builtinslib_test.py +3735 -0
  64. crosshair/libimpl/codecslib.py +86 -0
  65. crosshair/libimpl/codecslib_test.py +86 -0
  66. crosshair/libimpl/collectionslib.py +264 -0
  67. crosshair/libimpl/collectionslib_ch_test.py +252 -0
  68. crosshair/libimpl/collectionslib_test.py +332 -0
  69. crosshair/libimpl/copylib.py +23 -0
  70. crosshair/libimpl/copylib_test.py +18 -0
  71. crosshair/libimpl/datetimelib.py +2559 -0
  72. crosshair/libimpl/datetimelib_ch_test.py +354 -0
  73. crosshair/libimpl/datetimelib_test.py +112 -0
  74. crosshair/libimpl/decimallib.py +5257 -0
  75. crosshair/libimpl/decimallib_ch_test.py +78 -0
  76. crosshair/libimpl/decimallib_test.py +76 -0
  77. crosshair/libimpl/encodings/__init__.py +23 -0
  78. crosshair/libimpl/encodings/_encutil.py +187 -0
  79. crosshair/libimpl/encodings/ascii.py +44 -0
  80. crosshair/libimpl/encodings/latin_1.py +40 -0
  81. crosshair/libimpl/encodings/utf_8.py +93 -0
  82. crosshair/libimpl/encodings_ch_test.py +83 -0
  83. crosshair/libimpl/fractionlib.py +16 -0
  84. crosshair/libimpl/fractionlib_test.py +80 -0
  85. crosshair/libimpl/functoolslib.py +34 -0
  86. crosshair/libimpl/functoolslib_test.py +56 -0
  87. crosshair/libimpl/hashliblib.py +30 -0
  88. crosshair/libimpl/hashliblib_test.py +18 -0
  89. crosshair/libimpl/heapqlib.py +47 -0
  90. crosshair/libimpl/heapqlib_test.py +21 -0
  91. crosshair/libimpl/importliblib.py +18 -0
  92. crosshair/libimpl/importliblib_test.py +38 -0
  93. crosshair/libimpl/iolib.py +216 -0
  94. crosshair/libimpl/iolib_ch_test.py +128 -0
  95. crosshair/libimpl/iolib_test.py +19 -0
  96. crosshair/libimpl/ipaddresslib.py +8 -0
  97. crosshair/libimpl/itertoolslib.py +44 -0
  98. crosshair/libimpl/itertoolslib_test.py +44 -0
  99. crosshair/libimpl/jsonlib.py +984 -0
  100. crosshair/libimpl/jsonlib_ch_test.py +42 -0
  101. crosshair/libimpl/jsonlib_test.py +51 -0
  102. crosshair/libimpl/mathlib.py +179 -0
  103. crosshair/libimpl/mathlib_ch_test.py +44 -0
  104. crosshair/libimpl/mathlib_test.py +67 -0
  105. crosshair/libimpl/oslib.py +7 -0
  106. crosshair/libimpl/pathliblib_test.py +10 -0
  107. crosshair/libimpl/randomlib.py +178 -0
  108. crosshair/libimpl/randomlib_test.py +120 -0
  109. crosshair/libimpl/relib.py +846 -0
  110. crosshair/libimpl/relib_ch_test.py +169 -0
  111. crosshair/libimpl/relib_test.py +493 -0
  112. crosshair/libimpl/timelib.py +72 -0
  113. crosshair/libimpl/timelib_test.py +82 -0
  114. crosshair/libimpl/typeslib.py +15 -0
  115. crosshair/libimpl/typeslib_test.py +36 -0
  116. crosshair/libimpl/unicodedatalib.py +75 -0
  117. crosshair/libimpl/unicodedatalib_test.py +42 -0
  118. crosshair/libimpl/urlliblib.py +23 -0
  119. crosshair/libimpl/urlliblib_test.py +19 -0
  120. crosshair/libimpl/weakreflib.py +13 -0
  121. crosshair/libimpl/weakreflib_test.py +69 -0
  122. crosshair/libimpl/zliblib.py +15 -0
  123. crosshair/libimpl/zliblib_test.py +13 -0
  124. crosshair/lsp_server.py +261 -0
  125. crosshair/lsp_server_test.py +30 -0
  126. crosshair/main.py +973 -0
  127. crosshair/main_test.py +543 -0
  128. crosshair/objectproxy.py +376 -0
  129. crosshair/objectproxy_test.py +41 -0
  130. crosshair/opcode_intercept.py +601 -0
  131. crosshair/opcode_intercept_test.py +304 -0
  132. crosshair/options.py +218 -0
  133. crosshair/options_test.py +10 -0
  134. crosshair/patch_equivalence_test.py +75 -0
  135. crosshair/path_cover.py +209 -0
  136. crosshair/path_cover_test.py +138 -0
  137. crosshair/path_search.py +161 -0
  138. crosshair/path_search_test.py +52 -0
  139. crosshair/pathing_oracle.py +271 -0
  140. crosshair/pathing_oracle_test.py +21 -0
  141. crosshair/pure_importer.py +27 -0
  142. crosshair/pure_importer_test.py +16 -0
  143. crosshair/py.typed +0 -0
  144. crosshair/register_contract.py +273 -0
  145. crosshair/register_contract_test.py +190 -0
  146. crosshair/simplestructs.py +1165 -0
  147. crosshair/simplestructs_test.py +283 -0
  148. crosshair/smtlib.py +24 -0
  149. crosshair/smtlib_test.py +14 -0
  150. crosshair/statespace.py +1199 -0
  151. crosshair/statespace_test.py +108 -0
  152. crosshair/stubs_parser.py +352 -0
  153. crosshair/stubs_parser_test.py +43 -0
  154. crosshair/test_util.py +329 -0
  155. crosshair/test_util_test.py +26 -0
  156. crosshair/tools/__init__.py +0 -0
  157. crosshair/tools/check_help_in_doc.py +264 -0
  158. crosshair/tools/check_init_and_setup_coincide.py +119 -0
  159. crosshair/tools/generate_demo_table.py +127 -0
  160. crosshair/tracers.py +544 -0
  161. crosshair/tracers_test.py +154 -0
  162. crosshair/type_repo.py +151 -0
  163. crosshair/unicode_categories.py +589 -0
  164. crosshair/unicode_categories_test.py +27 -0
  165. crosshair/util.py +741 -0
  166. crosshair/util_test.py +173 -0
  167. crosshair/watcher.py +307 -0
  168. crosshair/watcher_test.py +107 -0
  169. crosshair/z3util.py +76 -0
  170. crosshair/z3util_test.py +11 -0
  171. crosshair_tool-0.0.99.dist-info/METADATA +144 -0
  172. crosshair_tool-0.0.99.dist-info/RECORD +176 -0
  173. crosshair_tool-0.0.99.dist-info/WHEEL +6 -0
  174. crosshair_tool-0.0.99.dist-info/entry_points.txt +3 -0
  175. crosshair_tool-0.0.99.dist-info/licenses/LICENSE +93 -0
  176. crosshair_tool-0.0.99.dist-info/top_level.txt +2 -0
@@ -0,0 +1,601 @@
1
+ import dis
2
+ import sys
3
+ import weakref
4
+ from collections import defaultdict
5
+ from collections.abc import MutableMapping, Set
6
+ from sys import version_info
7
+ from types import CodeType, FrameType
8
+ from typing import Any, Callable, Iterable, List, Mapping, Tuple, Union
9
+
10
+ from z3 import ExprRef # type: ignore
11
+
12
+ from crosshair.core import (
13
+ ATOMIC_IMMUTABLE_TYPES,
14
+ register_opcode_patch,
15
+ with_uniform_probabilities,
16
+ )
17
+ from crosshair.libimpl.builtinslib import (
18
+ AnySymbolicStr,
19
+ AtomicSymbolicValue,
20
+ ModelingDirector,
21
+ SymbolicBool,
22
+ SymbolicInt,
23
+ SymbolicList,
24
+ python_types_using_atomic_symbolics,
25
+ )
26
+ from crosshair.simplestructs import LinearSet, ShellMutableSet, SimpleDict, SliceView
27
+ from crosshair.statespace import context_statespace
28
+ from crosshair.tracers import (
29
+ COMPOSITE_TRACER,
30
+ NoTracing,
31
+ ResumedTracing,
32
+ TracingModule,
33
+ frame_stack_read,
34
+ frame_stack_write,
35
+ )
36
+ from crosshair.util import (
37
+ CROSSHAIR_EXTRA_ASSERTS,
38
+ CrossHairInternal,
39
+ CrossHairValue,
40
+ debug,
41
+ )
42
+ from crosshair.z3util import z3Not, z3Or
43
+
44
+ BINARY_SUBSCR = dis.opmap.get("BINARY_SUBSCR", 256)
45
+ BINARY_SLICE = dis.opmap.get("BINARY_SLICE", 256)
46
+ BUILD_STRING = dis.opmap["BUILD_STRING"]
47
+ COMPARE_OP = dis.opmap["COMPARE_OP"]
48
+ CONTAINS_OP = dis.opmap.get("CONTAINS_OP", 256)
49
+ FORMAT_VALUE = dis.opmap.get("FORMAT_VALUE", 256)
50
+ CONVERT_VALUE = dis.opmap.get("CONVERT_VALUE", 256)
51
+ MAP_ADD = dis.opmap["MAP_ADD"]
52
+ SET_ADD = dis.opmap["SET_ADD"]
53
+ UNARY_NOT = dis.opmap["UNARY_NOT"]
54
+ TO_BOOL = dis.opmap.get("TO_BOOL", 256)
55
+ IS_OP = dis.opmap.get("IS_OP", 256)
56
+ BINARY_MODULO = dis.opmap.get("BINARY_MODULO", 256)
57
+ BINARY_OP = dis.opmap.get("BINARY_OP", 256)
58
+ LOAD_COMMON_CONSTANT = dis.opmap.get("LOAD_COMMON_CONSTANT", 256)
59
+
60
+
61
+ def frame_op_arg(frame):
62
+ return frame.f_code.co_code[frame.f_lasti + 1] # TODO: account for EXTENDED_ARG?
63
+
64
+
65
+ _DEEPLY_CONCRETE_KEY_TYPES = (
66
+ int,
67
+ float,
68
+ str,
69
+ # Suble but important; when subscripting a Weak[Key|Value]Dictionary,
70
+ # we need to avoid creating a SimpleDict out of the backing dictionary.
71
+ # (because it can drop keys during iteration and fail)
72
+ weakref.ref,
73
+ )
74
+
75
+
76
+ class MultiSubscriptableContainer:
77
+ """Used for indexing a symbolic (non-slice) key into a concrete container"""
78
+
79
+ def __init__(self, container: Union[list, tuple, dict]):
80
+ self.container = container
81
+
82
+ def __getitem__(self, key: AtomicSymbolicValue) -> object:
83
+ with NoTracing():
84
+ space = context_statespace()
85
+ container = self.container
86
+ if isinstance(container, Mapping):
87
+ kv_pairs: Iterable[Tuple[Any, Any]] = container.items()
88
+ else:
89
+ kv_pairs = enumerate(container)
90
+
91
+ values_by_type = defaultdict(list)
92
+ values_by_id = {}
93
+ keys_by_value_id = defaultdict(list)
94
+ symbolic_for_pytype = space.extra(ModelingDirector).choose
95
+ for cur_key, cur_value in kv_pairs:
96
+ if (
97
+ isinstance(cur_value, AtomicSymbolicValue)
98
+ or type(cur_value) in python_types_using_atomic_symbolics()
99
+ ):
100
+ pytype = (
101
+ cur_value._pytype()
102
+ if isinstance(cur_value, AtomicSymbolicValue)
103
+ else type(cur_value)
104
+ )
105
+ # Some types like real-based float and symbolic types don't cover all values:
106
+ if (
107
+ symbolic_for_pytype(pytype)._smt_promote_literal(cur_value)
108
+ is not None
109
+ ):
110
+ values_by_type[pytype].append((cur_key, cur_value))
111
+ continue
112
+ # No symbolics cover this value, but we might still find repeated values:
113
+ values_by_id[id(cur_value)] = cur_value
114
+ keys_by_value_id[id(cur_value)].append(cur_key)
115
+ for value_type, cur_pairs in values_by_type.items():
116
+ hypothetical_result = symbolic_for_pytype(value_type)(
117
+ "item_" + space.uniq(), value_type
118
+ )
119
+ with ResumedTracing():
120
+ condition_pairs = []
121
+ for cur_key, cur_val in cur_pairs:
122
+ keys_equal = key == cur_key
123
+ values_equal = hypothetical_result == cur_val
124
+ with NoTracing():
125
+ if isinstance(keys_equal, SymbolicBool):
126
+ condition_pairs.append((keys_equal, values_equal))
127
+ elif keys_equal is False:
128
+ pass
129
+ else:
130
+ # (because the key must be symbolic, we don't ever expect raw True)
131
+ raise CrossHairInternal(
132
+ f"key comparison type: {type(keys_equal)} {keys_equal}"
133
+ )
134
+ if any(keys_equal for keys_equal, _ in condition_pairs):
135
+ space.add(any([all(pair) for pair in condition_pairs]))
136
+ return hypothetical_result
137
+
138
+ exprs_and_values: List[Tuple[ExprRef, object]] = []
139
+ for value_id, value in values_by_id.items():
140
+ keys_for_value = keys_by_value_id[value_id]
141
+ with ResumedTracing():
142
+ is_match = any([key == k for k in keys_for_value])
143
+ if isinstance(is_match, SymbolicBool):
144
+ exprs_and_values.append((is_match.var, value))
145
+ elif is_match:
146
+ return value
147
+ if exprs_and_values:
148
+ return space.smt_fanout(exprs_and_values, desc="multi_subscript")
149
+
150
+ if type(container) is dict:
151
+ raise KeyError # ( f"Key {key} not found in dict")
152
+ else:
153
+ raise IndexError # (f"Index {key} out of range for list/tuple of length {len(container)}")
154
+
155
+
156
+ class LoadCommonConstantInterceptor(TracingModule):
157
+ """
158
+ As of 3.14, the bytecode generation process generates optimizations
159
+ for builtins.any/all when invoked on a generator expression.
160
+ It essentially "inlines" the logic as bytecode.
161
+ We need to avoid this.
162
+ Before entering the optimized code path, it will check that the any/all
163
+ function is identity-equal to the original builtin, which is loaded using
164
+ the LOAD_COMMON_CONSTANT opcode.
165
+
166
+ This interceptor replaces that function with a proxy that functions
167
+ identically but is not identity-equal (so that we avoid the optimized
168
+ path),
169
+ """
170
+
171
+ opcodes_wanted = frozenset([LOAD_COMMON_CONSTANT])
172
+
173
+ def trace_op(self, frame, codeobj, codenum):
174
+ CONSTANT_BUILTIN_ALL = 3
175
+ CONSTANT_BUILTIN_ANY = 4
176
+ index = frame_op_arg(frame)
177
+
178
+ def post_op():
179
+ expected_fn = all if index == CONSTANT_BUILTIN_ALL else any
180
+ if CROSSHAIR_EXTRA_ASSERTS:
181
+ if frame_stack_read(frame, -1) is not expected_fn:
182
+ raise CrossHairInternal
183
+ frame_stack_write(frame, -1, lambda *a: expected_fn(*a))
184
+
185
+ if index == CONSTANT_BUILTIN_ALL or index == CONSTANT_BUILTIN_ANY:
186
+ COMPOSITE_TRACER.set_postop_callback(post_op, frame)
187
+
188
+
189
+ class SymbolicSubscriptInterceptor(TracingModule):
190
+ opcodes_wanted = frozenset([BINARY_SUBSCR, BINARY_OP])
191
+
192
+ def trace_op(self, frame, codeobj, codenum):
193
+ if codenum == BINARY_OP:
194
+ oparg = frame_op_arg(frame)
195
+ if oparg != 26: # subscript operator, NB_SUBSCR
196
+ return
197
+
198
+ key = frame_stack_read(frame, -1)
199
+ if isinstance(key, _DEEPLY_CONCRETE_KEY_TYPES):
200
+ return
201
+ # If we got this far, the index is likely symbolic (or perhaps a slice object)
202
+ container = frame_stack_read(frame, -2)
203
+ container_type = type(container)
204
+ if isinstance(key, AtomicSymbolicValue) and type(container) in (
205
+ tuple,
206
+ list,
207
+ dict,
208
+ ):
209
+ wrapped_container = MultiSubscriptableContainer(container)
210
+ frame_stack_write(frame, -2, wrapped_container)
211
+ elif container_type is dict:
212
+ # SimpleDict won't hash the keys it's given!
213
+ wrapped_dict = SimpleDict(list(container.items()))
214
+ frame_stack_write(frame, -2, wrapped_dict)
215
+ elif isinstance(key, slice) and container_type is list:
216
+ step = key.step
217
+ if isinstance(step, CrossHairValue) or step not in (None, 1):
218
+ return
219
+ start, stop = key.start, key.stop
220
+ if isinstance(start, SymbolicInt) or isinstance(stop, SymbolicInt):
221
+ view_wrapper = SliceView(container, 0, len(container))
222
+ frame_stack_write(frame, -2, SymbolicList(view_wrapper))
223
+
224
+
225
+ class SymbolicSliceInterceptor(TracingModule):
226
+ opcodes_wanted = frozenset([BINARY_SLICE])
227
+
228
+ def trace_op(
229
+ self, frame, codeobj, codenum, _concrete_index_types=(int, float, str)
230
+ ):
231
+ # Note that because this is called from inside a Python trace handler, tracing
232
+ # is automatically disabled, so there's no need for a `with NoTracing():` guard.
233
+ start = frame_stack_read(frame, -1)
234
+ stop = frame_stack_read(frame, -2)
235
+ if isinstance(start, _concrete_index_types) and isinstance(
236
+ stop, _concrete_index_types
237
+ ):
238
+ return
239
+ # If we got this far, the index is likely symbolic (or perhaps a slice object)
240
+ container = frame_stack_read(frame, -3)
241
+ container_type = type(container)
242
+ if container_type is list:
243
+ if isinstance(start, SymbolicInt) or isinstance(stop, SymbolicInt):
244
+ view_wrapper = SliceView(container, 0, len(container))
245
+ frame_stack_write(frame, -3, SymbolicList(view_wrapper))
246
+
247
+
248
+ class DeoptimizedContainer:
249
+ def __init__(self, container):
250
+ self.container = container
251
+
252
+ def __contains__(self, other):
253
+ return self.container.__contains__(other)
254
+
255
+
256
+ class SideEffectStashingHashable:
257
+ def __init__(self, fn: Callable):
258
+ self.fn = fn
259
+
260
+ def __hash__(self):
261
+ self.result = self.fn()
262
+ return 0
263
+
264
+
265
+ class DeoptimizedPercentFormattingStr:
266
+ def __init__(self, value):
267
+ self.value = value
268
+
269
+ def __mod__(self, other):
270
+ return self.value.__mod__(other)
271
+
272
+
273
+ class FormatStashingValue:
274
+ def __init__(self, value):
275
+ self.value = value
276
+
277
+ def __str__(self):
278
+ self.formatted = str(self.value)
279
+ return ""
280
+
281
+ def __format__(self, fmt: str):
282
+ self.formatted = format(self.value, fmt)
283
+ return ""
284
+
285
+ def __repr__(self) -> str:
286
+ self.formatted = repr(self.value)
287
+ return ""
288
+
289
+
290
+ class BoolStashingValue:
291
+ def __init__(self, value, negate):
292
+ self.value = value
293
+ self.negate = negate
294
+
295
+ def __bool__(self):
296
+ stashed_bool = self.value.__bool__()
297
+ with NoTracing():
298
+ if self.negate:
299
+ if isinstance(stashed_bool, SymbolicBool):
300
+ self.stashed_bool = SymbolicBool(z3Not(stashed_bool.var))
301
+ else:
302
+ self.stashed_bool = not stashed_bool
303
+ else:
304
+ self.stashed_bool = stashed_bool
305
+ return True
306
+
307
+
308
+ _CONTAINMENT_OP_TYPES = tuple(
309
+ i for (i, name) in enumerate(dis.cmp_op) if name in ("in", "not in")
310
+ )
311
+ assert len(_CONTAINMENT_OP_TYPES) in (0, 2)
312
+
313
+ _COMPARE_ISOP_TYPES = tuple(
314
+ i for (i, name) in enumerate(dis.cmp_op) if name in ("is", "is not")
315
+ )
316
+ assert len(_COMPARE_ISOP_TYPES) in (0, 2)
317
+
318
+
319
+ class ComparisonInterceptForwarder(TracingModule):
320
+
321
+ opcodes_wanted = frozenset([COMPARE_OP])
322
+
323
+ def trace_op(self, frame, codeobj, codenum):
324
+ # Python 3.8 used a general purpose comparison opcode.
325
+ # Forward to dedicated opcode handlers as appropriate.
326
+ compare_type = frame_op_arg(frame)
327
+ if compare_type in _CONTAINMENT_OP_TYPES:
328
+ ContainmentInterceptor.trace_op(None, frame, codeobj, codenum)
329
+ elif compare_type in _COMPARE_ISOP_TYPES:
330
+ IdentityInterceptor.trace_op(None, frame, codeobj, codenum)
331
+
332
+
333
+ class ContainmentInterceptor(TracingModule):
334
+
335
+ opcodes_wanted = frozenset([CONTAINS_OP])
336
+
337
+ def trace_op(self, frame, codeobj, codenum):
338
+ item = frame_stack_read(frame, -2)
339
+ if not isinstance(item, CrossHairValue):
340
+ return
341
+ container = frame_stack_read(frame, -1)
342
+ containertype = type(container)
343
+ new_container = None
344
+ if containertype is str:
345
+ new_container = DeoptimizedContainer(container)
346
+ elif containertype is set:
347
+ new_container = ShellMutableSet(LinearSet(container))
348
+ elif containertype is dict:
349
+ new_container = SimpleDict(list(container.items()))
350
+
351
+ if new_container is not None:
352
+ frame_stack_write(frame, -1, new_container)
353
+
354
+
355
+ class BuildStringInterceptor(TracingModule):
356
+ """
357
+ Adds symbolic handling for the BUILD_STRING opcode (used by f-strings).
358
+
359
+ BUILD_STRING concatenates strings from the stack is a fast, but unforgiving way:
360
+ it requires all the substrings to be real Python strings.
361
+ We work around this by replacing the substrings with empty strings, computing the
362
+ concatenation ourselves, and swaping our result in after the opcode completes.
363
+ """
364
+
365
+ opcodes_wanted = frozenset([BUILD_STRING])
366
+
367
+ def trace_op(self, frame, codeobj, codenum):
368
+ count = frame_op_arg(frame)
369
+ real_result = ""
370
+ for offset in range(-(count), 0):
371
+ substr = frame_stack_read(frame, offset)
372
+ if not isinstance(substr, (str, AnySymbolicStr)):
373
+ raise CrossHairInternal
374
+ # Because we know these are all symbolic or concrete strings, it's ok to
375
+ # not have tracing on when we do the concatenation here:
376
+ real_result += substr
377
+ frame_stack_write(frame, offset, "")
378
+
379
+ def post_op():
380
+ frame_stack_write(frame, -1, real_result)
381
+
382
+ COMPOSITE_TRACER.set_postop_callback(post_op, frame)
383
+
384
+
385
+ class FormatValueInterceptor(TracingModule):
386
+ """Avoid checks and realization during FORMAT_VALUE (used by f-strings)."""
387
+
388
+ # TODO: don't we need to handle FORMAT_SIMPLE and FORMAT_WITH_SPEC?
389
+ opcodes_wanted = frozenset([FORMAT_VALUE, CONVERT_VALUE])
390
+
391
+ def trace_op(self, frame, codeobj, codenum):
392
+ flags = frame_op_arg(frame)
393
+ value_idx = -2 if flags == 0x04 else -1
394
+ orig_obj = frame_stack_read(frame, value_idx)
395
+
396
+ # FORMAT_VALUE checks that results are concrete strings. So, we format via a
397
+ # a wrapper that returns an empty str, and then swap in the actual string later:
398
+
399
+ wrapper = FormatStashingValue(orig_obj)
400
+ if flags in (0x00, 0x01) and isinstance(orig_obj, AnySymbolicStr):
401
+ # Just use the symbolic string directly (don't bother formatting at all)
402
+ wrapper.formatted = orig_obj
403
+ frame_stack_write(frame, value_idx, "")
404
+ else:
405
+ frame_stack_write(frame, value_idx, wrapper)
406
+
407
+ def post_op():
408
+ frame_stack_write(frame, -1, wrapper.formatted)
409
+
410
+ COMPOSITE_TRACER.set_postop_callback(post_op, frame)
411
+
412
+
413
+ class MapAddInterceptor(TracingModule):
414
+ """De-optimize MAP_ADD over symbolics (used in dict comprehensions)."""
415
+
416
+ opcodes_wanted = frozenset([MAP_ADD])
417
+
418
+ def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
419
+ dict_offset = -(frame_op_arg(frame) + 2)
420
+ dict_obj = frame_stack_read(frame, dict_offset)
421
+ if not isinstance(dict_obj, (dict, MutableMapping)):
422
+ raise CrossHairInternal
423
+ # Key and value were swapped in Python 3.8
424
+ key_offset, value_offset = (-2, -1) if version_info >= (3, 8) else (-1, -2)
425
+ key = frame_stack_read(frame, key_offset)
426
+ value = frame_stack_read(frame, value_offset)
427
+ if isinstance(dict_obj, dict):
428
+ if type(key) in ATOMIC_IMMUTABLE_TYPES:
429
+ # Dict and key is (deeply) concrete; continue as normal.
430
+ return
431
+ else:
432
+ dict_obj = SimpleDict(list(dict_obj.items()))
433
+
434
+ # Have the interpreter do a fake assinment.
435
+ # While the fake assignment happens, we'll perform the real assignment secretly
436
+ # when Python hashes the fake key.
437
+ def do_real_assignment():
438
+ dict_obj[key] = value
439
+
440
+ frame_stack_write(frame, dict_offset, {})
441
+ frame_stack_write(frame, value_offset, 1)
442
+ frame_stack_write(
443
+ frame, key_offset, SideEffectStashingHashable(do_real_assignment)
444
+ )
445
+
446
+ # Afterwards, overwrite the interpreter's resulting dict with ours:
447
+ def post_op():
448
+ old_dict_obj = frame_stack_read(frame, dict_offset + 2)
449
+ if CROSSHAIR_EXTRA_ASSERTS and not isinstance(
450
+ old_dict_obj, (dict, MutableMapping)
451
+ ):
452
+ raise CrossHairInternal("interpreter stack corruption detected")
453
+ frame_stack_write(frame, dict_offset + 2, dict_obj)
454
+
455
+ COMPOSITE_TRACER.set_postop_callback(post_op, frame)
456
+
457
+
458
+ class ToBoolInterceptor(TracingModule):
459
+ """Retain symbolic booleans across the TO_BOOL operator."""
460
+
461
+ opcodes_wanted = frozenset([TO_BOOL])
462
+
463
+ def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
464
+ input_bool = frame_stack_read(frame, -1)
465
+ if not isinstance(input_bool, CrossHairValue):
466
+ return
467
+ if isinstance(input_bool, SymbolicBool):
468
+ # TODO: right now, we define __bool__ methods to perform realization.
469
+ # At some point, if that isn't the case, and we can remove this specialized
470
+ # branch for `SybolicBool`.
471
+ frame_stack_write(frame, -1, True)
472
+
473
+ def post_op():
474
+ frame_stack_write(frame, -1, input_bool)
475
+
476
+ else:
477
+ stashing_value = BoolStashingValue(input_bool, negate=False)
478
+ frame_stack_write(frame, -1, stashing_value)
479
+
480
+ def post_op():
481
+ frame_stack_write(frame, -1, stashing_value.stashed_bool)
482
+
483
+ COMPOSITE_TRACER.set_postop_callback(post_op, frame)
484
+
485
+
486
+ class NotInterceptor(TracingModule):
487
+ """Retain symbolic booleans across the `not` operator."""
488
+
489
+ opcodes_wanted = frozenset([UNARY_NOT])
490
+
491
+ def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
492
+ input_bool = frame_stack_read(frame, -1)
493
+ if not isinstance(input_bool, CrossHairValue):
494
+ return
495
+
496
+ if isinstance(input_bool, SymbolicBool):
497
+ # TODO: right now, we define __bool__ methods to perform realization.
498
+ # At some point, if that isn't the case, and we can remove this specialized
499
+ # branch for `SybolicBool`.
500
+ frame_stack_write(frame, -1, True)
501
+
502
+ def post_op():
503
+ frame_stack_write(frame, -1, SymbolicBool(z3Not(input_bool.var)))
504
+
505
+ else:
506
+ stashing_value = BoolStashingValue(input_bool, negate=True)
507
+ frame_stack_write(frame, -1, stashing_value)
508
+
509
+ def post_op():
510
+ frame_stack_write(frame, -1, stashing_value.stashed_bool)
511
+
512
+ COMPOSITE_TRACER.set_postop_callback(post_op, frame)
513
+
514
+
515
+ class SetAddInterceptor(TracingModule):
516
+ """De-optimize SET_ADD over symbolics (used in set comprehensions)."""
517
+
518
+ opcodes_wanted = frozenset([SET_ADD])
519
+
520
+ def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
521
+ set_offset = -(frame_op_arg(frame) + 1)
522
+ set_obj = frame_stack_read(frame, set_offset)
523
+ if not isinstance(set_obj, Set):
524
+ raise CrossHairInternal(type(set_obj))
525
+ item = frame_stack_read(frame, -1)
526
+ if isinstance(set_obj, set):
527
+ if isinstance(item, CrossHairValue):
528
+ set_obj = ShellMutableSet(set_obj)
529
+ else:
530
+ # Set and value are concrete; continue as normal.
531
+ return
532
+ # Have the interpreter do a fake addition, namely `set().add(1)`
533
+ dummy_set: Set = set()
534
+ frame_stack_write(frame, set_offset, dummy_set)
535
+ frame_stack_write(frame, -1, 1)
536
+
537
+ # And do our own addition separately:
538
+ set_obj.add(item)
539
+
540
+ # Later, overwrite the interpreter's result with ours:
541
+ def post_op():
542
+ if CROSSHAIR_EXTRA_ASSERTS:
543
+ to_replace = frame_stack_read(frame, set_offset + 1)
544
+ if to_replace is not dummy_set:
545
+ raise CrossHairInternal(
546
+ f"Found an instance of {type(to_replace)} where dummy set should be."
547
+ )
548
+ frame_stack_write(frame, set_offset + 1, set_obj)
549
+
550
+ COMPOSITE_TRACER.set_postop_callback(post_op, frame)
551
+
552
+
553
+ class IdentityInterceptor(TracingModule):
554
+ """Detect an "is" comparison to symbolics booleans"""
555
+
556
+ opcodes_wanted = frozenset([IS_OP])
557
+ # TODO: Adding support for an OptionalSymbolic would now be possible.
558
+ # TODO: it would be amazing to add symbolic enums and support comparison here
559
+
560
+ def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
561
+ arg1 = frame_stack_read(frame, -1)
562
+ arg2 = frame_stack_read(frame, -2)
563
+ if isinstance(arg1, SymbolicBool) and isinstance(arg2, (bool, SymbolicBool)):
564
+ frame_stack_write(frame, -1, arg1.__ch_realize__())
565
+ if isinstance(arg2, SymbolicBool) and isinstance(arg1, (bool, SymbolicBool)):
566
+ frame_stack_write(frame, -2, arg2.__ch_realize__())
567
+
568
+
569
+ class ModuloInterceptor(TracingModule):
570
+ opcodes_wanted = frozenset([BINARY_MODULO, BINARY_OP])
571
+ assert BINARY_MODULO != BINARY_OP
572
+
573
+ def trace_op(self, frame, codeobj, codenum):
574
+ left = frame_stack_read(frame, -2)
575
+ from crosshair.util import debug
576
+
577
+ if isinstance(left, str):
578
+ if codenum == BINARY_OP:
579
+ oparg = frame_op_arg(frame)
580
+ if oparg != 6: # modulo operator, NB_REMAINDER
581
+ return
582
+ frame_stack_write(frame, -2, DeoptimizedPercentFormattingStr(left))
583
+
584
+
585
+ def make_registrations():
586
+ register_opcode_patch(SymbolicSubscriptInterceptor())
587
+ if sys.version_info >= (3, 12):
588
+ register_opcode_patch(SymbolicSliceInterceptor())
589
+ if sys.version_info < (3, 9):
590
+ register_opcode_patch(ComparisonInterceptForwarder())
591
+ if sys.version_info >= (3, 14):
592
+ register_opcode_patch(LoadCommonConstantInterceptor())
593
+ register_opcode_patch(ContainmentInterceptor())
594
+ register_opcode_patch(BuildStringInterceptor())
595
+ register_opcode_patch(FormatValueInterceptor())
596
+ register_opcode_patch(MapAddInterceptor())
597
+ # register_opcode_patch(ToBoolInterceptor())
598
+ register_opcode_patch(NotInterceptor())
599
+ register_opcode_patch(SetAddInterceptor())
600
+ register_opcode_patch(IdentityInterceptor())
601
+ register_opcode_patch(ModuloInterceptor())