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
crosshair/core.py CHANGED
@@ -11,7 +11,6 @@
11
11
  import enum
12
12
  import functools
13
13
  import inspect
14
- import itertools
15
14
  import linecache
16
15
  import os.path
17
16
  import sys
@@ -23,6 +22,8 @@ from collections import ChainMap, defaultdict, deque
23
22
  from contextlib import ExitStack
24
23
  from dataclasses import dataclass, replace
25
24
  from inspect import BoundArguments, Signature, isabstract
25
+ from time import monotonic
26
+ from traceback import StackSummary, extract_stack, extract_tb, format_exc
26
27
  from typing import (
27
28
  Any,
28
29
  Callable,
@@ -33,11 +34,9 @@ from typing import (
33
34
  List,
34
35
  Mapping,
35
36
  MutableMapping,
36
- NewType,
37
37
  Optional,
38
38
  Sequence,
39
39
  Set,
40
- Sized,
41
40
  Tuple,
42
41
  Type,
43
42
  TypeVar,
@@ -73,12 +72,11 @@ from crosshair.fnutil import (
73
72
  resolve_signature,
74
73
  )
75
74
  from crosshair.options import DEFAULT_OPTIONS, AnalysisOptions, AnalysisOptionSet
76
- from crosshair.register_contract import get_contract
75
+ from crosshair.register_contract import clear_contract_registrations, get_contract
77
76
  from crosshair.statespace import (
78
77
  AnalysisMessage,
79
78
  CallAnalysis,
80
79
  MessageType,
81
- NotDeterministic,
82
80
  RootNode,
83
81
  SimpleStateSpace,
84
82
  StateSpace,
@@ -95,6 +93,7 @@ from crosshair.tracers import (
95
93
  PatchingModule,
96
94
  ResumedTracing,
97
95
  TracingModule,
96
+ check_opcode_support,
98
97
  is_tracing,
99
98
  )
100
99
  from crosshair.type_repo import get_subclass_map
@@ -102,13 +101,16 @@ from crosshair.util import (
102
101
  ATOMIC_IMMUTABLE_TYPES,
103
102
  UNABLE_TO_REPR_TEXT,
104
103
  AttributeHolder,
105
- CrosshairInternal,
104
+ CrossHairInternal,
106
105
  CrosshairUnsupported,
106
+ CrossHairValue,
107
107
  EvalFriendlyReprContext,
108
108
  IdKeyedDict,
109
109
  IgnoreAttempt,
110
+ NotDeterministic,
110
111
  ReferencedIdentifier,
111
112
  UnexploredPath,
113
+ ch_stack,
112
114
  debug,
113
115
  eval_friendly_repr,
114
116
  format_boundargs,
@@ -121,28 +123,31 @@ from crosshair.util import (
121
123
  samefile,
122
124
  smtlib_typename,
123
125
  sourcelines,
124
- test_stack,
125
126
  type_args_of,
126
127
  warn,
127
128
  )
128
129
 
130
+ if sys.version_info >= (3, 12):
131
+ from typing import TypeAliasType
132
+
133
+ TypeAliasTypes = (TypeAliasType,)
134
+ else:
135
+ TypeAliasTypes = ()
136
+
137
+
129
138
  _MISSING = object()
130
139
 
131
140
 
132
141
  _OPCODE_PATCHES: List[TracingModule] = []
133
142
 
134
143
  _PATCH_REGISTRATIONS: Dict[Callable, Callable] = {}
135
- _PATCH_FN_TYPE_REGISTRATIONS: Dict[type, Callable] = {}
136
144
 
137
145
 
138
- class Patched(TracingModule):
146
+ class Patched:
139
147
  def __enter__(self):
140
148
  COMPOSITE_TRACER.patching_module.add(_PATCH_REGISTRATIONS)
141
- COMPOSITE_TRACER.patching_module.fn_type_overrides = (
142
- _PATCH_FN_TYPE_REGISTRATIONS
143
- )
144
149
  if len(_OPCODE_PATCHES) == 0:
145
- raise CrosshairInternal("Opcode patches haven't been loaded yet.")
150
+ raise CrossHairInternal("Opcode patches haven't been loaded yet.")
146
151
  for module in _OPCODE_PATCHES:
147
152
  COMPOSITE_TRACER.push_module(module)
148
153
  self.pushed = _OPCODE_PATCHES[:]
@@ -152,7 +157,6 @@ class Patched(TracingModule):
152
157
  for module in reversed(self.pushed):
153
158
  COMPOSITE_TRACER.pop_config(module)
154
159
  COMPOSITE_TRACER.patching_module.pop(_PATCH_REGISTRATIONS)
155
- COMPOSITE_TRACER.patching_module.fn_type_overrides = {}
156
160
  return False
157
161
 
158
162
 
@@ -174,11 +178,46 @@ class _StandaloneStatespace(ExitStack):
174
178
  standalone_statespace = _StandaloneStatespace()
175
179
 
176
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
+
177
216
  class ExceptionFilter:
178
217
  analysis: CallAnalysis
179
218
  ignore: bool = False
180
219
  ignore_with_confirmation: bool = False
181
- user_exc: Optional[Tuple[BaseException, traceback.StackSummary]] = None
220
+ user_exc: Optional[Tuple[BaseException, StackSummary]] = None
182
221
  expected_exceptions: Tuple[Type[BaseException], ...]
183
222
 
184
223
  def __init__(
@@ -212,24 +251,24 @@ class ExceptionFilter:
212
251
  self.ignore = True
213
252
  self.analysis = CallAnalysis(VerificationStatus.CONFIRMED)
214
253
  return True
215
- if isinstance(exc_value, TypeError):
216
- exc_str = str(exc_value)
217
- if (
218
- "SymbolicStr" in exc_str
219
- or "SymbolicInt" in exc_str
220
- or "SymbolicFloat" in exc_str
221
- or "__hash__ method should return an integer" in exc_str
222
- or "expected string or bytes-like object" in exc_str
223
- ):
224
- # Ideally we'd attempt literal strings after encountering this.
225
- # See https://github.com/pschanely/CrossHair/issues/8
226
- debug("Proxy intolerace at: ", traceback.format_exc())
227
- raise CrosshairUnsupported("Detected proxy intolerance: " + exc_str)
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")
228
259
  if isinstance(exc_value, (Exception, PreconditionFailed)):
229
- if isinstance(exc_value, z3.Z3Exception):
230
- return False # internal issue: re-raise
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
231
268
  # Most other issues are assumed to be user-facing exceptions:
232
- self.user_exc = (exc_value, traceback.extract_tb(sys.exc_info()[2]))
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))
233
272
  self.analysis = CallAnalysis(VerificationStatus.REFUTED)
234
273
  return True # suppress user-level exception
235
274
  return False # re-raise resource and system issues
@@ -246,13 +285,9 @@ def realize(value: Any) -> Any:
246
285
  return value
247
286
 
248
287
 
249
- def deep_realize(value: _T) -> _T:
288
+ def deep_realize(value: _T, memo: Optional[Dict] = None) -> _T:
250
289
  with NoTracing():
251
- return deepcopyext(value, CopyMode.REALIZE, {})
252
-
253
-
254
- class CrossHairValue:
255
- pass
290
+ return deepcopyext(value, CopyMode.REALIZE, {} if memo is None else memo)
256
291
 
257
292
 
258
293
  def normalize_pytype(typ: Type) -> Type:
@@ -278,7 +313,7 @@ def normalize_pytype(typ: Type) -> Type:
278
313
 
279
314
  def python_type(o: object) -> Type:
280
315
  if is_tracing():
281
- raise CrosshairInternal("should not be tracing while getting pytype")
316
+ raise CrossHairInternal("should not be tracing while getting pytype")
282
317
  if hasattr(type(o), "__ch_pytype__"):
283
318
  obj_type = o.__ch_pytype__() # type: ignore
284
319
  if hasattr(obj_type, "__origin__"):
@@ -288,17 +323,51 @@ def python_type(o: object) -> Type:
288
323
  return type(o)
289
324
 
290
325
 
291
- def with_realized_args(fn: Callable) -> Callable:
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
+
292
338
  def realizer(*a, **kw):
293
339
  with NoTracing():
294
- a = map(realize, a)
295
- kw = {k: realize(v) for (k, v) in kw.items()}
296
- return fn(*a, **kw)
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)
297
344
 
298
345
  functools.update_wrapper(realizer, fn)
299
346
  return realizer
300
347
 
301
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
+
302
371
  def with_symbolic_self(symbolic_cls: Type, fn: Callable):
303
372
  def call_with_symbolic_self(self, *args, **kwargs):
304
373
  with NoTracing():
@@ -308,7 +377,8 @@ def with_symbolic_self(symbolic_cls: Type, fn: Callable):
308
377
  elif any(isinstance(a, CrossHairValue) for a in args) or (
309
378
  kwargs and any(isinstance(a, CrossHairValue) for a in kwargs.values())
310
379
  ):
311
- self = symbolic_cls._smt_promote_literal(self)
380
+ # NOTE: _ch_create_from_literal is suppoerted for very few types right now
381
+ self = symbolic_cls._ch_create_from_literal(self)
312
382
  target_fn = getattr(symbolic_cls, fn.__name__)
313
383
  else:
314
384
  args = map(realize, args)
@@ -359,7 +429,7 @@ def choose_type(space: StateSpace, from_type: Type, varname: str) -> Optional[Ty
359
429
  probability_true=probability_true,
360
430
  ):
361
431
  return typ
362
- raise CrosshairInternal
432
+ raise CrossHairInternal
363
433
 
364
434
 
365
435
  def get_constructor_signature(cls: Type) -> Optional[inspect.Signature]:
@@ -369,24 +439,30 @@ def get_constructor_signature(cls: Type) -> Optional[inspect.Signature]:
369
439
  sig = resolve_signature(cls)
370
440
  if isinstance(sig, inspect.Signature):
371
441
  return sig
442
+
443
+ applicable_sigs: List[Signature] = []
372
444
  new_fn = cls.__new__
373
- sig = resolve_signature(new_fn)
374
- # TODO: merge the type signatures of __init__ and __new__, pulling the
375
- # most specific types from each.
376
- # Fall back to __init__ if we don't have types:
377
- if isinstance(sig, str) or all(
378
- p.annotation is Signature.empty for p in sig.parameters.values()
379
- ):
380
- init_fn = cls.__init__
381
- if init_fn is not object.__init__:
382
- sig = resolve_signature(init_fn)
383
- else:
384
- return inspect.Signature([])
385
- if isinstance(sig, inspect.Signature):
386
- # strip first argument
387
- newparams = list(sig.parameters.values())[1:]
388
- return sig.replace(parameters=newparams)
389
- return None
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)
390
466
 
391
467
 
392
468
  _TYPE_HINTS = IdKeyedDict()
@@ -425,7 +501,7 @@ def proxy_for_class(typ: Type, varname: str) -> object:
425
501
  # postconditions can be invalidated when the class has invariants.
426
502
  raise IgnoreAttempt
427
503
  except Exception as e:
428
- debug("Root-cause type construction traceback:", test_stack(e.__traceback__))
504
+ debug("Root-cause type construction traceback:", ch_stack(currently_handling=e))
429
505
  raise CrosshairUnsupported(
430
506
  f"error constructing {typename} instance: {name_of_type(type(e))}: {e}",
431
507
  ) from e
@@ -440,24 +516,32 @@ def proxy_for_class(typ: Type, varname: str) -> object:
440
516
  return f"{repr(typ)}({format_boundargs(realized_args)})"
441
517
 
442
518
  reprer.reprs[obj] = regenerate_construction_string
443
-
444
- debug("repr register lazy", hex(id(obj)), typename)
445
519
  return obj
446
520
 
447
521
 
448
522
  def register_patch(entity: Callable, patch_value: Callable):
449
523
  if entity in _PATCH_REGISTRATIONS:
450
- raise CrosshairInternal(f"Doubly registered patch: {entity}")
524
+ raise CrossHairInternal(f"Doubly registered patch: {entity}")
451
525
  _PATCH_REGISTRATIONS[entity] = patch_value
452
526
 
453
527
 
454
- def register_fn_type_patch(typ: type, patch_value: Callable[[Callable], Callable]):
455
- if typ in _PATCH_FN_TYPE_REGISTRATIONS:
456
- raise CrosshairInternal(f"Doubly registered fn type patch: {typ}")
457
- _PATCH_FN_TYPE_REGISTRATIONS[typ] = patch_value
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()
458
536
 
459
537
 
460
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
+
461
545
  _OPCODE_PATCHES.append(module)
462
546
 
463
547
 
@@ -473,15 +557,18 @@ class SymbolicFactory:
473
557
  self.pytype: Any = pytype
474
558
  self.varname = varname
475
559
 
560
+ def get_suffixed_varname(self, suffix: str):
561
+ return self.varname + suffix + self.space.uniq()
562
+
476
563
  @overload
477
564
  def __call__(
478
565
  self, typ: Callable[..., _T], suffix: str = "", allow_subtypes: bool = True
479
- ) -> _T:
480
- ...
566
+ ) -> _T: ...
481
567
 
482
568
  @overload
483
- def __call__(self, typ: Any, suffix: str = "", allow_subtypes: bool = True) -> Any:
484
- ...
569
+ def __call__(
570
+ self, typ: Any, suffix: str = "", allow_subtypes: bool = True
571
+ ) -> Any: ...
485
572
 
486
573
  def __call__(self, typ, suffix: str = "", allow_subtypes: bool = True):
487
574
  """
@@ -497,12 +584,12 @@ class SymbolicFactory:
497
584
  """
498
585
  return proxy_for_type(
499
586
  typ,
500
- self.varname + suffix + self.space.uniq(),
587
+ self.get_suffixed_varname(suffix),
501
588
  allow_subtypes=allow_subtypes,
502
589
  )
503
590
 
504
591
 
505
- _SIMPLE_PROXIES: MutableMapping[object, Callable] = {}
592
+ _SIMPLE_PROXIES: MutableMapping[type, Callable] = {}
506
593
 
507
594
  SymbolicCreationCallback = Union[
508
595
  # Sadly Callable[] doesn't support variable arguments. Just enumerate:
@@ -528,7 +615,7 @@ def register_type(typ: Type, creator: SymbolicCreationCallback) -> None:
528
615
  typ
529
616
  ), f'Only origin types may be registered, not "{typ}": try "{origin_of(typ)}" instead.'
530
617
  if typ in _SIMPLE_PROXIES:
531
- raise CrosshairInternal(f'Duplicate type "{typ}" registered')
618
+ raise CrossHairInternal(f'Duplicate type "{typ}" registered')
532
619
  _SIMPLE_PROXIES[typ] = creator
533
620
 
534
621
 
@@ -566,8 +653,7 @@ def proxy_for_type(
566
653
  typ: Callable[..., _T],
567
654
  varname: str,
568
655
  allow_subtypes: bool = False,
569
- ) -> _T:
570
- ...
656
+ ) -> _T: ...
571
657
 
572
658
 
573
659
  @overload
@@ -575,8 +661,7 @@ def proxy_for_type(
575
661
  typ: Any,
576
662
  varname: str,
577
663
  allow_subtypes: bool = False,
578
- ) -> Any:
579
- ...
664
+ ) -> Any: ...
580
665
 
581
666
 
582
667
  def proxy_for_type(
@@ -589,6 +674,11 @@ def proxy_for_type(
589
674
  typ = normalize_pytype(typ)
590
675
  origin = origin_of(typ)
591
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
+
592
682
  # special cases
593
683
  if isinstance(typ, type) and issubclass(typ, enum.Enum):
594
684
  enum_values = list(typ) # type:ignore
@@ -598,8 +688,10 @@ def proxy_for_type(
598
688
  if space.smt_fork(desc="choose_enum_" + str(enum_value)):
599
689
  return enum_value
600
690
  return enum_values[-1]
601
- # It's easy to forget to import crosshair.core_and_libs; check:
602
- assert _SIMPLE_PROXIES, "No proxy type registrations exist"
691
+ if not _SIMPLE_PROXIES:
692
+ from crosshair.core_and_libs import _make_registrations
693
+
694
+ _make_registrations()
603
695
  proxy_factory = _SIMPLE_PROXIES.get(origin)
604
696
  if proxy_factory:
605
697
  recursive_proxy_factory = SymbolicFactory(space, typ, varname)
@@ -618,7 +710,7 @@ _ARG_GENERATION_RENAMES: Dict[str, Callable] = {}
618
710
 
619
711
  def gen_args(sig: inspect.Signature) -> inspect.BoundArguments:
620
712
  if is_tracing():
621
- raise CrosshairInternal
713
+ raise CrossHairInternal
622
714
  args = sig.bind_partial()
623
715
  space = context_statespace()
624
716
  for param in sig.parameters.values():
@@ -717,7 +809,7 @@ class ConditionCheckable(Checkable):
717
809
  "assuming preconditions: ",
718
810
  ",".join([p.expr_source for p in conditions.pre]),
719
811
  )
720
- options.deadline = time.monotonic() + options.per_condition_timeout
812
+ options.deadline = monotonic() + options.per_condition_timeout
721
813
 
722
814
  with condition_parser(options.analysis_kind):
723
815
  analysis = analyze_calltree(options, conditions)
@@ -765,6 +857,9 @@ class ClampedCheckable(Checkable):
765
857
  self.cls_file = filename
766
858
  self.cls_start_line = start_line
767
859
 
860
+ def __repr__(self) -> str:
861
+ return f"ClampedCheckable({self.checkable})"
862
+
768
863
  def analyze(self) -> Iterable[AnalysisMessage]:
769
864
  cls_file = self.cls_file
770
865
  ret = []
@@ -803,7 +898,7 @@ def analyze_any(
803
898
  elif inspect.ismodule(entity):
804
899
  yield from analyze_module(cast(types.ModuleType, entity), options)
805
900
  else:
806
- raise CrosshairInternal("Entity type not analyzable: " + str(type(entity)))
901
+ raise CrossHairInternal("Entity type not analyzable: " + str(type(entity)))
807
902
 
808
903
 
809
904
  def analyze_module(
@@ -1045,6 +1140,47 @@ class CallTreeAnalysis:
1045
1140
  num_confirmed_paths: int = 0
1046
1141
 
1047
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
+
1048
1184
  def analyze_calltree(
1049
1185
  options: AnalysisOptions, conditions: Conditions
1050
1186
  ) -> CallTreeAnalysis:
@@ -1068,9 +1204,9 @@ def analyze_calltree(
1068
1204
  max_uninteresting_iterations = options.get_max_uninteresting_iterations()
1069
1205
  patched = Patched()
1070
1206
  # TODO clean up how encofrced conditions works here?
1071
- with enforced_conditions, patched:
1207
+ with patched:
1072
1208
  for i in range(1, options.max_iterations + 1):
1073
- start = time.monotonic()
1209
+ start = monotonic()
1074
1210
  if start > options.deadline:
1075
1211
  debug("Exceeded condition timeout, stopping")
1076
1212
  break
@@ -1107,6 +1243,25 @@ def analyze_calltree(
1107
1243
  call_analysis.failing_precondition_reason
1108
1244
  )
1109
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
+ )
1110
1265
  except UnexploredPath:
1111
1266
  call_analysis = CallAnalysis(VerificationStatus.UNKNOWN)
1112
1267
  except IgnoreAttempt:
@@ -1183,7 +1338,7 @@ PathCompeltionCallback = Callable[
1183
1338
  BoundArguments,
1184
1339
  Any,
1185
1340
  Optional[BaseException],
1186
- Optional[traceback.StackSummary],
1341
+ Optional[StackSummary],
1187
1342
  ],
1188
1343
  bool,
1189
1344
  ]
@@ -1199,12 +1354,12 @@ def explore_paths(
1199
1354
  """
1200
1355
  Runs a path exploration for use cases beyond invariant checking.
1201
1356
  """
1202
- condition_start = time.monotonic()
1357
+ condition_start = monotonic()
1203
1358
  breakout = False
1204
1359
  max_uninteresting_iterations = options.get_max_uninteresting_iterations()
1205
1360
  for i in range(1, options.max_iterations + 1):
1206
1361
  debug("Iteration ", i)
1207
- itr_start = time.monotonic()
1362
+ itr_start = monotonic()
1208
1363
  if itr_start > condition_start + options.per_condition_timeout:
1209
1364
  debug(
1210
1365
  "Stopping due to --per_condition_timeout=",
@@ -1225,7 +1380,7 @@ def explore_paths(
1225
1380
  args = deepcopyext(pre_args, CopyMode.REGULAR, {})
1226
1381
  ret: object = None
1227
1382
  user_exc: Optional[BaseException] = None
1228
- user_exc_stack: Optional[traceback.StackSummary] = None
1383
+ user_exc_stack: Optional[StackSummary] = None
1229
1384
  with ExceptionFilter() as efilter, ResumedTracing():
1230
1385
  ret = fn(args)
1231
1386
  if efilter.user_exc:
@@ -1266,102 +1421,6 @@ def explore_paths(
1266
1421
  break
1267
1422
 
1268
1423
 
1269
- class UnEqual:
1270
- pass
1271
-
1272
-
1273
- _UNEQUAL = UnEqual()
1274
-
1275
-
1276
- def deep_eq(old_val: object, new_val: object, visiting: Set[Tuple[int, int]]) -> bool:
1277
- # TODO: test just about all of this
1278
- if old_val is new_val:
1279
- return True
1280
- if type(old_val) != type(new_val):
1281
- return False
1282
- visit_key = (id(old_val), id(new_val))
1283
- if visit_key in visiting:
1284
- return True
1285
- visiting.add(visit_key)
1286
- try:
1287
- with NoTracing():
1288
- is_ch_value = isinstance(old_val, CrossHairValue)
1289
- if is_ch_value:
1290
- return old_val == new_val
1291
- elif hasattr(old_val, "__dict__") and hasattr(new_val, "__dict__"):
1292
- return deep_eq(old_val.__dict__, new_val.__dict__, visiting)
1293
- elif isinstance(old_val, dict):
1294
- assert isinstance(new_val, dict)
1295
- for key in set(itertools.chain(old_val.keys(), *new_val.keys())):
1296
- if (key in old_val) ^ (key in new_val):
1297
- return False
1298
- if not deep_eq(
1299
- old_val.get(key, _UNEQUAL), new_val.get(key, _UNEQUAL), visiting
1300
- ):
1301
- return False
1302
- return True
1303
- elif isinstance(old_val, Iterable):
1304
- assert isinstance(new_val, Sized)
1305
- if isinstance(old_val, Sized):
1306
- if len(old_val) != len(new_val):
1307
- return False
1308
- assert isinstance(new_val, Iterable)
1309
- return all(
1310
- deep_eq(o, n, visiting)
1311
- for (o, n) in itertools.zip_longest(
1312
- old_val, new_val, fillvalue=_UNEQUAL
1313
- )
1314
- )
1315
- elif type(old_val) is object:
1316
- # Plain object instances are close enough to equal for our purposes
1317
- return True
1318
- else:
1319
- # hopefully this is just ints, bools, etc
1320
- return old_val == new_val
1321
- finally:
1322
- visiting.remove(visit_key)
1323
-
1324
-
1325
- class MessageGenerator:
1326
- def __init__(self, fn: Callable):
1327
- self.filename = ""
1328
- if hasattr(fn, "__code__"):
1329
- code_obj = fn.__code__
1330
- self.filename = code_obj.co_filename
1331
- self.start_lineno = code_obj.co_firstlineno
1332
- _, _, lines = sourcelines(fn)
1333
- self.end_lineno = self.start_lineno + len(lines)
1334
-
1335
- def make(
1336
- self,
1337
- message_type: MessageType,
1338
- detail: str,
1339
- suggested_filename: Optional[str],
1340
- suggested_lineno: int,
1341
- tb: str,
1342
- ) -> AnalysisMessage:
1343
- if (
1344
- suggested_filename is not None
1345
- and (os.path.abspath(suggested_filename) == os.path.abspath(self.filename))
1346
- and (self.start_lineno <= suggested_lineno <= self.end_lineno)
1347
- ):
1348
- return AnalysisMessage(
1349
- message_type, detail, suggested_filename, suggested_lineno, 0, tb
1350
- )
1351
- else:
1352
- exprline = "<unknown>"
1353
- if suggested_filename is not None:
1354
- lines = linecache.getlines(suggested_filename)
1355
- try:
1356
- exprline = lines[suggested_lineno - 1].strip()
1357
- except IndexError:
1358
- pass
1359
- detail = f'"{exprline}" yields {detail}'
1360
- return AnalysisMessage(
1361
- message_type, detail, self.filename, self.start_lineno, 0, tb
1362
- )
1363
-
1364
-
1365
1424
  def make_counterexample_message(
1366
1425
  conditions: Conditions, args: BoundArguments, return_val: object = None
1367
1426
  ) -> str:
@@ -1457,11 +1516,11 @@ def attempt_call(
1457
1516
  detail = name_of_type(type(e)) + ": " + str(e)
1458
1517
  tb_desc = tb.format()
1459
1518
  frame_filename, frame_lineno = frame_summary_for_fn(conditions.src_fn, tb)
1460
- if not isinstance(e, NotDeterministic):
1461
- with ResumedTracing():
1462
- space.detach_path()
1463
- detail += " " + make_counterexample_message(conditions, original_args)
1464
- debug("exception while evaluating function body:", detail, tb_desc)
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))
1465
1524
  return CallAnalysis(
1466
1525
  VerificationStatus.REFUTED,
1467
1526
  [
@@ -1481,10 +1540,8 @@ def attempt_call(
1481
1540
  and argname not in conditions.mutable_args
1482
1541
  ):
1483
1542
  old_val, new_val = original_args.arguments[argname], argval
1484
- # TODO: Do we really need custom equality here? Would love to drop that
1485
- # `deep_eq` function.
1486
1543
  with ResumedTracing():
1487
- if not deep_eq(old_val, new_val, set()):
1544
+ if old_val != new_val:
1488
1545
  space.detach_path()
1489
1546
  detail = 'Argument "{}" is not marked as mutable, but changed from {} to {}'.format(
1490
1547
  argname, old_val, new_val
@@ -1511,14 +1568,13 @@ def attempt_call(
1511
1568
  elif efilter.user_exc is not None:
1512
1569
  (e, tb) = efilter.user_exc
1513
1570
  detail = name_of_type(type(e)) + ": " + str(e)
1514
- if not isinstance(e, NotDeterministic):
1515
- with ResumedTracing():
1516
- space.detach_path()
1517
- detail += " " + make_counterexample_message(
1518
- conditions, original_args, __return__
1519
- )
1571
+ with ResumedTracing():
1572
+ space.detach_path(e)
1573
+ detail += " " + make_counterexample_message(
1574
+ conditions, original_args, __return__
1575
+ )
1520
1576
  debug("exception while calling postcondition:", detail)
1521
- debug("exception traceback:", test_stack(tb))
1577
+ debug("exception traceback:", ch_stack(tb))
1522
1578
  failures = [
1523
1579
  msg_gen.make(
1524
1580
  MessageType.POST_ERR,
@@ -1570,7 +1626,7 @@ def _mutability_testing_hash(o: object) -> int:
1570
1626
 
1571
1627
  def is_deeply_immutable(o: object) -> bool:
1572
1628
  if not is_tracing():
1573
- raise CrosshairInternal("is_deeply_immutable must be run with tracing enabled")
1629
+ raise CrossHairInternal("is_deeply_immutable must be run with tracing enabled")
1574
1630
  orig_modules = COMPOSITE_TRACER.get_modules()
1575
1631
  hash_intercept_module = PatchingModule({hash: _mutability_testing_hash})
1576
1632
  for module in reversed(orig_modules):