crosshair-tool 0.0.56__cp39-cp39-macosx_11_0_arm64.whl → 0.0.100__cp39-cp39-macosx_11_0_arm64.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 (123) hide show
  1. _crosshair_tracers.cpython-39-darwin.so +0 -0
  2. crosshair/__init__.py +1 -1
  3. crosshair/_mark_stacks.h +51 -24
  4. crosshair/_tracers.h +9 -5
  5. crosshair/_tracers_test.py +19 -9
  6. crosshair/auditwall.py +9 -8
  7. crosshair/auditwall_test.py +31 -19
  8. crosshair/codeconfig.py +3 -2
  9. crosshair/condition_parser.py +17 -133
  10. crosshair/condition_parser_test.py +54 -96
  11. crosshair/conftest.py +1 -1
  12. crosshair/copyext.py +91 -22
  13. crosshair/copyext_test.py +33 -0
  14. crosshair/core.py +259 -203
  15. crosshair/core_and_libs.py +20 -0
  16. crosshair/core_regestered_types_test.py +82 -0
  17. crosshair/core_test.py +693 -664
  18. crosshair/diff_behavior.py +76 -21
  19. crosshair/diff_behavior_test.py +132 -23
  20. crosshair/dynamic_typing.py +128 -18
  21. crosshair/dynamic_typing_test.py +91 -4
  22. crosshair/enforce.py +1 -6
  23. crosshair/enforce_test.py +15 -23
  24. crosshair/examples/check_examples_test.py +2 -1
  25. crosshair/fnutil.py +2 -3
  26. crosshair/fnutil_test.py +0 -7
  27. crosshair/fuzz_core_test.py +70 -83
  28. crosshair/libimpl/arraylib.py +10 -7
  29. crosshair/libimpl/binascii_ch_test.py +30 -0
  30. crosshair/libimpl/binascii_test.py +67 -0
  31. crosshair/libimpl/binasciilib.py +150 -0
  32. crosshair/libimpl/bisectlib_test.py +5 -5
  33. crosshair/libimpl/builtinslib.py +1002 -682
  34. crosshair/libimpl/builtinslib_ch_test.py +108 -30
  35. crosshair/libimpl/builtinslib_test.py +431 -143
  36. crosshair/libimpl/codecslib.py +22 -2
  37. crosshair/libimpl/codecslib_test.py +41 -9
  38. crosshair/libimpl/collectionslib.py +44 -8
  39. crosshair/libimpl/collectionslib_test.py +108 -20
  40. crosshair/libimpl/copylib.py +1 -1
  41. crosshair/libimpl/copylib_test.py +18 -0
  42. crosshair/libimpl/datetimelib.py +84 -67
  43. crosshair/libimpl/datetimelib_ch_test.py +12 -7
  44. crosshair/libimpl/datetimelib_test.py +5 -6
  45. crosshair/libimpl/decimallib.py +5257 -0
  46. crosshair/libimpl/decimallib_ch_test.py +78 -0
  47. crosshair/libimpl/decimallib_test.py +76 -0
  48. crosshair/libimpl/encodings/_encutil.py +21 -11
  49. crosshair/libimpl/fractionlib.py +16 -0
  50. crosshair/libimpl/fractionlib_test.py +80 -0
  51. crosshair/libimpl/functoolslib.py +19 -7
  52. crosshair/libimpl/functoolslib_test.py +22 -6
  53. crosshair/libimpl/hashliblib.py +30 -0
  54. crosshair/libimpl/hashliblib_test.py +18 -0
  55. crosshair/libimpl/heapqlib.py +32 -5
  56. crosshair/libimpl/heapqlib_test.py +15 -12
  57. crosshair/libimpl/iolib.py +7 -4
  58. crosshair/libimpl/ipaddresslib.py +8 -0
  59. crosshair/libimpl/itertoolslib_test.py +1 -1
  60. crosshair/libimpl/mathlib.py +165 -2
  61. crosshair/libimpl/mathlib_ch_test.py +44 -0
  62. crosshair/libimpl/mathlib_test.py +59 -16
  63. crosshair/libimpl/oslib.py +7 -0
  64. crosshair/libimpl/pathliblib_test.py +10 -0
  65. crosshair/libimpl/randomlib.py +1 -0
  66. crosshair/libimpl/randomlib_test.py +6 -4
  67. crosshair/libimpl/relib.py +180 -59
  68. crosshair/libimpl/relib_ch_test.py +26 -2
  69. crosshair/libimpl/relib_test.py +77 -14
  70. crosshair/libimpl/timelib.py +35 -13
  71. crosshair/libimpl/timelib_test.py +13 -3
  72. crosshair/libimpl/typeslib.py +15 -0
  73. crosshair/libimpl/typeslib_test.py +36 -0
  74. crosshair/libimpl/unicodedatalib_test.py +3 -3
  75. crosshair/libimpl/weakreflib.py +13 -0
  76. crosshair/libimpl/weakreflib_test.py +69 -0
  77. crosshair/libimpl/zliblib.py +15 -0
  78. crosshair/libimpl/zliblib_test.py +13 -0
  79. crosshair/lsp_server.py +21 -10
  80. crosshair/main.py +48 -28
  81. crosshair/main_test.py +59 -14
  82. crosshair/objectproxy.py +39 -14
  83. crosshair/objectproxy_test.py +27 -13
  84. crosshair/opcode_intercept.py +212 -24
  85. crosshair/opcode_intercept_test.py +172 -18
  86. crosshair/options.py +0 -1
  87. crosshair/patch_equivalence_test.py +5 -21
  88. crosshair/path_cover.py +7 -5
  89. crosshair/path_search.py +6 -4
  90. crosshair/path_search_test.py +1 -2
  91. crosshair/pathing_oracle.py +53 -10
  92. crosshair/pathing_oracle_test.py +21 -0
  93. crosshair/pure_importer_test.py +5 -21
  94. crosshair/register_contract.py +16 -6
  95. crosshair/register_contract_test.py +2 -14
  96. crosshair/simplestructs.py +154 -85
  97. crosshair/simplestructs_test.py +16 -2
  98. crosshair/smtlib.py +24 -0
  99. crosshair/smtlib_test.py +14 -0
  100. crosshair/statespace.py +319 -196
  101. crosshair/statespace_test.py +45 -0
  102. crosshair/stubs_parser.py +0 -2
  103. crosshair/test_util.py +87 -25
  104. crosshair/test_util_test.py +26 -0
  105. crosshair/tools/check_init_and_setup_coincide.py +0 -3
  106. crosshair/tools/generate_demo_table.py +2 -2
  107. crosshair/tracers.py +141 -49
  108. crosshair/type_repo.py +11 -4
  109. crosshair/unicode_categories.py +1 -0
  110. crosshair/util.py +158 -76
  111. crosshair/util_test.py +13 -20
  112. crosshair/watcher.py +4 -4
  113. crosshair/z3util.py +1 -1
  114. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/METADATA +45 -36
  115. crosshair_tool-0.0.100.dist-info/RECORD +176 -0
  116. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/WHEEL +2 -1
  117. crosshair/examples/hypothesis/__init__.py +0 -2
  118. crosshair/examples/hypothesis/bugs_detected/simple_strategies.py +0 -74
  119. crosshair_tool-0.0.56.dist-info/RECORD +0 -152
  120. /crosshair/{examples/hypothesis/bugs_detected/__init__.py → py.typed} +0 -0
  121. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/entry_points.txt +0 -0
  122. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info/licenses}/LICENSE +0 -0
  123. {crosshair_tool-0.0.56.dist-info → crosshair_tool-0.0.100.dist-info}/top_level.txt +0 -0
@@ -1,29 +1,47 @@
1
1
  import dis
2
2
  import sys
3
+ import weakref
4
+ from collections import defaultdict
3
5
  from collections.abc import MutableMapping, Set
4
6
  from sys import version_info
5
7
  from types import CodeType, FrameType
6
- from typing import Callable
8
+ from typing import Any, Callable, Iterable, List, Mapping, Tuple, Union
7
9
 
8
- from crosshair.core import ATOMIC_IMMUTABLE_TYPES, CrossHairValue, register_opcode_patch
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
+ )
9
17
  from crosshair.libimpl.builtinslib import (
10
18
  AnySymbolicStr,
19
+ AtomicSymbolicValue,
20
+ ModelingDirector,
11
21
  SymbolicBool,
12
22
  SymbolicInt,
13
23
  SymbolicList,
24
+ python_types_using_atomic_symbolics,
14
25
  )
15
26
  from crosshair.simplestructs import LinearSet, ShellMutableSet, SimpleDict, SliceView
27
+ from crosshair.statespace import context_statespace
16
28
  from crosshair.tracers import (
17
29
  COMPOSITE_TRACER,
18
30
  NoTracing,
31
+ ResumedTracing,
19
32
  TracingModule,
20
33
  frame_stack_read,
21
34
  frame_stack_write,
22
35
  )
23
- from crosshair.util import CrosshairInternal
24
- from crosshair.z3util import z3Not
36
+ from crosshair.util import (
37
+ CROSSHAIR_EXTRA_ASSERTS,
38
+ CrossHairInternal,
39
+ CrossHairValue,
40
+ debug,
41
+ )
42
+ from crosshair.z3util import z3Not, z3Or
25
43
 
26
- BINARY_SUBSCR = dis.opmap["BINARY_SUBSCR"]
44
+ BINARY_SUBSCR = dis.opmap.get("BINARY_SUBSCR", 256)
27
45
  BINARY_SLICE = dis.opmap.get("BINARY_SLICE", 256)
28
46
  BUILD_STRING = dis.opmap["BUILD_STRING"]
29
47
  COMPARE_OP = dis.opmap["COMPARE_OP"]
@@ -35,32 +53,166 @@ SET_ADD = dis.opmap["SET_ADD"]
35
53
  UNARY_NOT = dis.opmap["UNARY_NOT"]
36
54
  TO_BOOL = dis.opmap.get("TO_BOOL", 256)
37
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)
38
59
 
39
60
 
40
61
  def frame_op_arg(frame):
41
62
  return frame.f_code.co_code[frame.f_lasti + 1] # TODO: account for EXTENDED_ARG?
42
63
 
43
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
+
44
189
  class SymbolicSubscriptInterceptor(TracingModule):
45
- opcodes_wanted = frozenset([BINARY_SUBSCR])
190
+ opcodes_wanted = frozenset([BINARY_SUBSCR, BINARY_OP])
46
191
 
47
192
  def trace_op(self, frame, codeobj, codenum):
48
- # Note that because this is called from inside a Python trace handler, tracing
49
- # is automatically disabled, so there's no need for a `with NoTracing():` guard.
193
+ if codenum == BINARY_OP:
194
+ oparg = frame_op_arg(frame)
195
+ if oparg != 26: # subscript operator, NB_SUBSCR
196
+ return
197
+
50
198
  key = frame_stack_read(frame, -1)
51
- if isinstance(key, (int, float, str)):
199
+ if isinstance(key, _DEEPLY_CONCRETE_KEY_TYPES):
52
200
  return
53
201
  # If we got this far, the index is likely symbolic (or perhaps a slice object)
54
202
  container = frame_stack_read(frame, -2)
55
203
  container_type = type(container)
56
- if container_type is dict:
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:
57
212
  # SimpleDict won't hash the keys it's given!
58
213
  wrapped_dict = SimpleDict(list(container.items()))
59
214
  frame_stack_write(frame, -2, wrapped_dict)
60
- elif container_type is list:
61
- if not isinstance(key, slice):
62
- # Nothing useful to do with concrete list and symbolic numeric index.
63
- return
215
+ elif isinstance(key, slice) and container_type is list:
64
216
  step = key.step
65
217
  if isinstance(step, CrossHairValue) or step not in (None, 1):
66
218
  return
@@ -110,6 +262,14 @@ class SideEffectStashingHashable:
110
262
  return 0
111
263
 
112
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
+
113
273
  class FormatStashingValue:
114
274
  def __init__(self, value):
115
275
  self.value = value
@@ -210,7 +370,7 @@ class BuildStringInterceptor(TracingModule):
210
370
  for offset in range(-(count), 0):
211
371
  substr = frame_stack_read(frame, offset)
212
372
  if not isinstance(substr, (str, AnySymbolicStr)):
213
- raise CrosshairInternal
373
+ raise CrossHairInternal
214
374
  # Because we know these are all symbolic or concrete strings, it's ok to
215
375
  # not have tracing on when we do the concatenation here:
216
376
  real_result += substr
@@ -225,6 +385,7 @@ class BuildStringInterceptor(TracingModule):
225
385
  class FormatValueInterceptor(TracingModule):
226
386
  """Avoid checks and realization during FORMAT_VALUE (used by f-strings)."""
227
387
 
388
+ # TODO: don't we need to handle FORMAT_SIMPLE and FORMAT_WITH_SPEC?
228
389
  opcodes_wanted = frozenset([FORMAT_VALUE, CONVERT_VALUE])
229
390
 
230
391
  def trace_op(self, frame, codeobj, codenum):
@@ -258,7 +419,7 @@ class MapAddInterceptor(TracingModule):
258
419
  dict_offset = -(frame_op_arg(frame) + 2)
259
420
  dict_obj = frame_stack_read(frame, dict_offset)
260
421
  if not isinstance(dict_obj, (dict, MutableMapping)):
261
- raise CrosshairInternal
422
+ raise CrossHairInternal
262
423
  # Key and value were swapped in Python 3.8
263
424
  key_offset, value_offset = (-2, -1) if version_info >= (3, 8) else (-1, -2)
264
425
  key = frame_stack_read(frame, key_offset)
@@ -266,7 +427,6 @@ class MapAddInterceptor(TracingModule):
266
427
  if isinstance(dict_obj, dict):
267
428
  if type(key) in ATOMIC_IMMUTABLE_TYPES:
268
429
  # Dict and key is (deeply) concrete; continue as normal.
269
- COMPOSITE_TRACER.set_postop_callback(None, frame)
270
430
  return
271
431
  else:
272
432
  dict_obj = SimpleDict(list(dict_obj.items()))
@@ -285,6 +445,11 @@ class MapAddInterceptor(TracingModule):
285
445
 
286
446
  # Afterwards, overwrite the interpreter's resulting dict with ours:
287
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")
288
453
  frame_stack_write(frame, dict_offset + 2, dict_obj)
289
454
 
290
455
  COMPOSITE_TRACER.set_postop_callback(post_op, frame)
@@ -298,7 +463,6 @@ class ToBoolInterceptor(TracingModule):
298
463
  def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
299
464
  input_bool = frame_stack_read(frame, -1)
300
465
  if not isinstance(input_bool, CrossHairValue):
301
- COMPOSITE_TRACER.set_postop_callback(None, frame)
302
466
  return
303
467
  if isinstance(input_bool, SymbolicBool):
304
468
  # TODO: right now, we define __bool__ methods to perform realization.
@@ -327,7 +491,6 @@ class NotInterceptor(TracingModule):
327
491
  def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
328
492
  input_bool = frame_stack_read(frame, -1)
329
493
  if not isinstance(input_bool, CrossHairValue):
330
- COMPOSITE_TRACER.set_postop_callback(None, frame)
331
494
  return
332
495
 
333
496
  if isinstance(input_bool, SymbolicBool):
@@ -358,17 +521,17 @@ class SetAddInterceptor(TracingModule):
358
521
  set_offset = -(frame_op_arg(frame) + 1)
359
522
  set_obj = frame_stack_read(frame, set_offset)
360
523
  if not isinstance(set_obj, Set):
361
- raise CrosshairInternal(type(set_obj))
524
+ raise CrossHairInternal(type(set_obj))
362
525
  item = frame_stack_read(frame, -1)
363
526
  if isinstance(set_obj, set):
364
527
  if isinstance(item, CrossHairValue):
365
528
  set_obj = ShellMutableSet(set_obj)
366
529
  else:
367
530
  # Set and value are concrete; continue as normal.
368
- COMPOSITE_TRACER.set_postop_callback(None, frame)
369
531
  return
370
532
  # Have the interpreter do a fake addition, namely `set().add(1)`
371
- frame_stack_write(frame, set_offset, set())
533
+ dummy_set: Set = set()
534
+ frame_stack_write(frame, set_offset, dummy_set)
372
535
  frame_stack_write(frame, -1, 1)
373
536
 
374
537
  # And do our own addition separately:
@@ -376,6 +539,12 @@ class SetAddInterceptor(TracingModule):
376
539
 
377
540
  # Later, overwrite the interpreter's result with ours:
378
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
+ )
379
548
  frame_stack_write(frame, set_offset + 1, set_obj)
380
549
 
381
550
  COMPOSITE_TRACER.set_postop_callback(post_op, frame)
@@ -391,18 +560,36 @@ class IdentityInterceptor(TracingModule):
391
560
  def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
392
561
  arg1 = frame_stack_read(frame, -1)
393
562
  arg2 = frame_stack_read(frame, -2)
394
- if isinstance(arg1, SymbolicBool):
563
+ if isinstance(arg1, SymbolicBool) and isinstance(arg2, (bool, SymbolicBool)):
395
564
  frame_stack_write(frame, -1, arg1.__ch_realize__())
396
- if isinstance(arg2, SymbolicBool):
565
+ if isinstance(arg2, SymbolicBool) and isinstance(arg1, (bool, SymbolicBool)):
397
566
  frame_stack_write(frame, -2, arg2.__ch_realize__())
398
567
 
399
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
+
400
585
  def make_registrations():
401
586
  register_opcode_patch(SymbolicSubscriptInterceptor())
402
587
  if sys.version_info >= (3, 12):
403
588
  register_opcode_patch(SymbolicSliceInterceptor())
404
589
  if sys.version_info < (3, 9):
405
590
  register_opcode_patch(ComparisonInterceptForwarder())
591
+ if sys.version_info >= (3, 14):
592
+ register_opcode_patch(LoadCommonConstantInterceptor())
406
593
  register_opcode_patch(ContainmentInterceptor())
407
594
  register_opcode_patch(BuildStringInterceptor())
408
595
  register_opcode_patch(FormatValueInterceptor())
@@ -411,3 +598,4 @@ def make_registrations():
411
598
  register_opcode_patch(NotInterceptor())
412
599
  register_opcode_patch(SetAddInterceptor())
413
600
  register_opcode_patch(IdentityInterceptor())
601
+ register_opcode_patch(ModuloInterceptor())
@@ -1,11 +1,21 @@
1
+ import math
1
2
  import sys
3
+ from abc import ABCMeta
2
4
  from typing import List, Set
3
5
 
4
6
  import pytest
5
7
 
6
8
  from crosshair.core_and_libs import NoTracing, proxy_for_type, standalone_statespace
7
- from crosshair.statespace import POST_FAIL, MessageType
9
+ from crosshair.libimpl.builtinslib import (
10
+ ModelingDirector,
11
+ RealBasedSymbolicFloat,
12
+ SymbolicBool,
13
+ SymbolicInt,
14
+ SymbolicType,
15
+ )
16
+ from crosshair.statespace import POST_FAIL
8
17
  from crosshair.test_util import check_states
18
+ from crosshair.tracers import ResumedTracing
9
19
  from crosshair.z3util import z3And
10
20
 
11
21
 
@@ -22,6 +32,101 @@ def test_dict_index():
22
32
  check_states(numstr, POST_FAIL)
23
33
 
24
34
 
35
+ def test_dict_index_without_realization(space):
36
+ class WithMeta(metaclass=ABCMeta):
37
+ pass
38
+
39
+ space.extra(ModelingDirector).global_representations[float] = RealBasedSymbolicFloat
40
+ a = {
41
+ -1: WithMeta,
42
+ # ^ tests regression: isinstance(WithMeta(), type) but type(WithMeta) != type
43
+ 0: list,
44
+ 1.0: 10.0,
45
+ 2: 20,
46
+ 3: 30,
47
+ 4: 40,
48
+ ("complex", "key"): 50,
49
+ 6: math.inf,
50
+ 7: math.inf,
51
+ }
52
+ int_key = proxy_for_type(int, "int_key")
53
+ int_key2 = proxy_for_type(int, "int_key2")
54
+ int_key3 = proxy_for_type(int, "int_key3")
55
+ float_key = RealBasedSymbolicFloat("float_key")
56
+ float_key2 = RealBasedSymbolicFloat("float_key2")
57
+ with ResumedTracing():
58
+ # Try some concrete values out first:
59
+ assert a[("complex", "key")] == 50
60
+ assert a[6] == float("inf")
61
+ try:
62
+ a[42]
63
+ assert False, "Expected KeyError for missing key 42"
64
+ except KeyError:
65
+ pass
66
+
67
+ space.add(2 <= int_key)
68
+ space.add(int_key <= 4)
69
+ int_result = a[int_key]
70
+ assert space.is_possible(int_result == 20)
71
+ assert space.is_possible(int_result == 40)
72
+ assert not space.is_possible(int_result == 10)
73
+ space.add(float_key == 1.0)
74
+ float_result = a[float_key]
75
+ assert space.is_possible(float_result == 10.0)
76
+ assert not space.is_possible(float_result == 42.0)
77
+ space.add(float_key2 == 2.0)
78
+ float_result2 = a[float_key2]
79
+ assert space.is_possible(float_result2 == 20)
80
+ space.add(int_key2 == 0)
81
+ int_result2 = a[int_key2]
82
+ assert int_result2 == list
83
+ space.add(any([int_key3 == 6, int_key3 == 7]))
84
+ inf_result = a[int_key3]
85
+ assert inf_result is math.inf
86
+ assert isinstance(int_result, SymbolicInt)
87
+ assert isinstance(float_result, RealBasedSymbolicFloat)
88
+ assert isinstance(float_result2, SymbolicInt)
89
+ assert isinstance(int_result2, SymbolicType)
90
+
91
+
92
+ def test_dict_symbolic_index_miss(space):
93
+ a = {6: 60, 7: 70}
94
+ x = proxy_for_type(int, "x")
95
+ with ResumedTracing():
96
+ space.add(x <= 4)
97
+ with pytest.raises(KeyError):
98
+ result = a[x]
99
+
100
+
101
+ def test_concrete_list_with_symbolic_index_simple(space):
102
+ haystack = [False] * 13 + [True] + [False] * 11
103
+
104
+ idx = proxy_for_type(int, "idx")
105
+ with ResumedTracing():
106
+ space.add(0 <= idx)
107
+ space.add(idx < len(haystack))
108
+ ret = haystack[idx]
109
+ assert isinstance(ret, SymbolicBool)
110
+ with ResumedTracing():
111
+ assert space.is_possible(idx == 13)
112
+ assert space.is_possible(idx == 12)
113
+ space.add(ret)
114
+ assert not space.is_possible(idx == 12)
115
+
116
+
117
+ def test_concrete_list_with_symbolic_index_unhashable_values(space):
118
+ o1 = dict()
119
+ options = [o1, o1, o1]
120
+ idx = proxy_for_type(int, "idx")
121
+ with ResumedTracing():
122
+ space.add(0 <= idx)
123
+ space.add(idx < 3)
124
+ ret = options[idx]
125
+ assert ret is o1
126
+ assert space.is_possible(idx == 0)
127
+ assert space.is_possible(idx == 2)
128
+
129
+
25
130
  def test_dict_key_containment():
26
131
  abc = {"two": 2, "four": 4, "six": 6}
27
132
 
@@ -34,12 +139,12 @@ def test_dict_key_containment():
34
139
  check_states(numstr, POST_FAIL)
35
140
 
36
141
 
37
- def test_dict_comprehension():
142
+ def test_dict_comprehension_basic():
38
143
  with standalone_statespace as space:
39
144
  with NoTracing():
40
145
  x = proxy_for_type(int, "x")
41
- space.add(x.var >= 40)
42
- space.add(x.var < 50)
146
+ space.add(x >= 40)
147
+ space.add(x < 50)
43
148
  d = {k: v for k, v in ((35, 3), (x, 4))}
44
149
  with NoTracing():
45
150
  assert type(d) is not dict
@@ -48,8 +153,8 @@ def test_dict_comprehension():
48
153
  continue
49
154
  with NoTracing():
50
155
  assert type(k) is not int
51
- assert space.is_possible((k == 43).var)
52
- assert space.is_possible((k == 48).var)
156
+ assert space.is_possible(k == 43)
157
+ assert space.is_possible(k == 48)
53
158
 
54
159
 
55
160
  def test_dict_comprehension_traces_during_custom_hash():
@@ -75,9 +180,8 @@ def test_dict_comprehension_traces_during_custom_hash():
75
180
  # There is only one item:
76
181
  assert len(d) == 1
77
182
  # TODO: In theory, we shouldn't need to realize the string here (but we are):
78
- # with NoTracing():
79
- # assert space.is_possible(mystr.__len__().var == 0)
80
- # assert space.is_possible(mystr.__len__().var == 1)
183
+ # assert space.is_possible(mystr.__len__() == 0)
184
+ # assert space.is_possible(mystr.__len__() == 1)
81
185
 
82
186
 
83
187
  def test_dict_comprehension_e2e():
@@ -90,15 +194,17 @@ def test_dict_comprehension_e2e():
90
194
  check_states(f, POST_FAIL)
91
195
 
92
196
 
197
+ @pytest.mark.skipif(
198
+ sys.version_info >= (3, 13), reason="Negation opcode changed; TODO: fix!"
199
+ )
93
200
  def test_not_operator_on_bool():
94
201
  with standalone_statespace as space:
95
202
  with NoTracing():
96
203
  boolval = proxy_for_type(bool, "boolval")
97
- intlist = proxy_for_type(List[int], "intlist")
98
204
  inverseval = not boolval
205
+ assert space.is_possible(inverseval)
99
206
  with NoTracing():
100
207
  assert type(inverseval) is not bool
101
- assert space.is_possible(inverseval.var)
102
208
  assert not space.is_possible(z3And(boolval.var, inverseval.var))
103
209
 
104
210
 
@@ -106,18 +212,18 @@ def test_not_operator_on_non_bool():
106
212
  with standalone_statespace as space:
107
213
  with NoTracing():
108
214
  intlist = proxy_for_type(List[int], "intlist")
109
- space.add(intlist.__len__().var == 0)
215
+ space.add(intlist.__len__() == 0)
110
216
  notList = not intlist
111
217
  with NoTracing():
112
218
  assert notList
113
219
 
114
220
 
115
- def test_set_comprehension():
221
+ def test_set_comprehension_basic():
116
222
  with standalone_statespace as space:
117
223
  with NoTracing():
118
224
  x = proxy_for_type(int, "x")
119
- space.add(x.var >= 40)
120
- space.add(x.var < 50)
225
+ space.add(x >= 40)
226
+ space.add(x < 50)
121
227
  result_set = {k for k in (35, x)}
122
228
  with NoTracing():
123
229
  assert type(result_set) is not set
@@ -126,8 +232,8 @@ def test_set_comprehension():
126
232
  continue
127
233
  with NoTracing():
128
234
  assert type(k) is not int
129
- assert space.is_possible((k == 43).var)
130
- assert space.is_possible((k == 48).var)
235
+ assert space.is_possible(k == 43)
236
+ assert space.is_possible(k == 48)
131
237
 
132
238
 
133
239
  def test_set_comprehension_e2e():
@@ -140,11 +246,59 @@ def test_set_comprehension_e2e():
140
246
  check_states(f, POST_FAIL)
141
247
 
142
248
 
249
+ def test_trace_disabling_at_jump_targets(space):
250
+ # This replicates a corruption of the interpreter stack in 3.12
251
+ # under a specific bytecode layout.
252
+ #
253
+ # The origial issue was caused by neglecting to keep sys.monitor probes
254
+ # alive (for post-op callbacks) that could be jumped to from other
255
+ # locations.
256
+ _global_type_lookupx = {
257
+ 1: 1,
258
+ bool: 2,
259
+ 3: 3,
260
+ }
261
+ with ResumedTracing():
262
+ _ = {
263
+ k: v
264
+ for k, v in _global_type_lookupx.items() # <- a new line has to be here (yes, the generated bytecode differs!)
265
+ if k == bool # The iteration filter needs to alternate
266
+ }
267
+
268
+
143
269
  # TODO: we could implement identity comparisons on 3.8 by intercepting COMPARE_OP
144
270
  @pytest.mark.skipif(sys.version_info < (3, 9), reason="IS_OP is new in Python 3.9")
145
271
  def test_identity_operator_on_booleans():
146
272
  with standalone_statespace as space:
147
273
  with NoTracing():
148
274
  b1 = proxy_for_type(bool, "b1")
149
- space.add(b1.var.__eq__(True))
275
+ space.add(b1)
150
276
  assert b1 is True
277
+
278
+
279
+ @pytest.mark.skipif(sys.version_info < (3, 9), reason="IS_OP is new in Python 3.9")
280
+ def test_identity_operator_does_not_realize_on_differing_types():
281
+ with standalone_statespace as space:
282
+ with NoTracing():
283
+ b1 = proxy_for_type(bool, "b1")
284
+ choices_made_at_start = len(space.choices_made)
285
+ space.add(b1)
286
+ fourty_two = 42 # assignment just to avoid lint errors
287
+ b1 is fourty_two
288
+ assert len(space.choices_made) == choices_made_at_start
289
+
290
+
291
+ class IExplodeOnRepr:
292
+ def __repr__(self):
293
+ raise ValueError("boom")
294
+
295
+
296
+ def test_postop_callback_skipped_on_exception_handler_jump(space):
297
+ with ResumedTracing():
298
+ elements = IExplodeOnRepr()
299
+ try:
300
+ ret = f"these are them: {elements!r}"
301
+ except ValueError: # pragma: no cover
302
+ ret = None
303
+ # need to do something(anything) with elements so that it's on the stack:
304
+ type(elements)
crosshair/options.py CHANGED
@@ -12,7 +12,6 @@ class AnalysisKind(enum.Enum):
12
12
  PEP316 = "PEP316"
13
13
  icontract = "icontract"
14
14
  deal = "deal"
15
- hypothesis = "hypothesis"
16
15
 
17
16
  def __repr__(self):
18
17
  return f"AnalysisKind.{self.name}"