crosshair-tool 0.0.83__cp39-cp39-macosx_10_9_universal2.whl → 0.0.85__cp39-cp39-macosx_10_9_universal2.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.

Potentially problematic release.


This version of crosshair-tool might be problematic. Click here for more details.

Files changed (46) hide show
  1. _crosshair_tracers.cpython-39-darwin.so +0 -0
  2. crosshair/__init__.py +1 -1
  3. crosshair/_mark_stacks.h +0 -25
  4. crosshair/_tracers.h +2 -0
  5. crosshair/_tracers_test.py +8 -2
  6. crosshair/auditwall.py +0 -1
  7. crosshair/auditwall_test.py +5 -0
  8. crosshair/condition_parser.py +5 -5
  9. crosshair/condition_parser_test.py +50 -63
  10. crosshair/copyext.py +23 -7
  11. crosshair/copyext_test.py +11 -1
  12. crosshair/core.py +23 -17
  13. crosshair/core_test.py +625 -584
  14. crosshair/diff_behavior_test.py +14 -21
  15. crosshair/dynamic_typing.py +90 -1
  16. crosshair/dynamic_typing_test.py +73 -1
  17. crosshair/enforce_test.py +15 -22
  18. crosshair/fnutil_test.py +4 -8
  19. crosshair/libimpl/arraylib.py +2 -5
  20. crosshair/libimpl/binasciilib.py +2 -3
  21. crosshair/libimpl/builtinslib.py +28 -21
  22. crosshair/libimpl/builtinslib_test.py +1 -8
  23. crosshair/libimpl/collectionslib.py +18 -3
  24. crosshair/libimpl/collectionslib_test.py +89 -15
  25. crosshair/libimpl/encodings/_encutil.py +8 -3
  26. crosshair/libimpl/mathlib_test.py +0 -7
  27. crosshair/libimpl/relib_ch_test.py +2 -2
  28. crosshair/libimpl/timelib.py +34 -15
  29. crosshair/libimpl/timelib_test.py +12 -2
  30. crosshair/lsp_server.py +1 -1
  31. crosshair/main.py +3 -1
  32. crosshair/objectproxy_test.py +7 -11
  33. crosshair/opcode_intercept.py +24 -8
  34. crosshair/opcode_intercept_test.py +13 -2
  35. crosshair/py.typed +0 -0
  36. crosshair/tracers.py +27 -9
  37. crosshair/type_repo.py +2 -2
  38. crosshair/unicode_categories.py +1 -0
  39. crosshair/util.py +45 -16
  40. crosshair/watcher.py +2 -2
  41. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/METADATA +4 -3
  42. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/RECORD +46 -45
  43. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/WHEEL +1 -1
  44. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/entry_points.txt +0 -0
  45. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info/licenses}/LICENSE +0 -0
  46. {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  import collections
2
+ import sys
2
3
  from typing import (
3
4
  Any,
4
5
  Callable,
@@ -125,8 +126,19 @@ class ListBasedDeque(collections.abc.MutableSequence, CrossHairValue, Generic[T]
125
126
  prefix.reverse()
126
127
  self._contents = prefix + self._contents
127
128
 
128
- def index(self, item: T, *bounds) -> int:
129
- return self._contents.index(item, *bounds)
129
+ if sys.version_info >= (3, 14):
130
+
131
+ def index(self, item: T, *bounds) -> int:
132
+ try:
133
+ return self._contents.index(item, *bounds)
134
+ except ValueError as exc:
135
+ exc.args = ("deque.index(x): x not in deque",)
136
+ raise
137
+
138
+ else:
139
+
140
+ def index(self, item: T, *bounds) -> int:
141
+ return self._contents.index(item, *bounds)
130
142
 
131
143
  def insert(self, index: int, item: T) -> None:
132
144
  self._contents.insert(index, item)
@@ -245,5 +257,8 @@ def make_registrations():
245
257
 
246
258
  register_type(collections.abc.MutableSet, lambda p, t=Any: p(Set[t])) # type: ignore
247
259
 
248
- register_type(collections.abc.ByteString, lambda p: p(bytes))
260
+ if sys.version_info < (3, 14):
261
+ register_type(collections.abc.ByteString, lambda p: p(bytes))
262
+ if sys.version_info >= (3, 12):
263
+ register_type(collections.abc.Buffer, lambda p: p(bytes))
249
264
  register_type(collections.abc.Hashable, lambda p: p(int))
@@ -1,14 +1,24 @@
1
- import collections
1
+ import re
2
+ import sys
3
+ from collections import Counter, defaultdict, deque, namedtuple
2
4
  from copy import deepcopy
3
- from typing import Counter, DefaultDict, Deque, Tuple
5
+ from inspect import Parameter, Signature
6
+ from typing import Counter, DefaultDict, Deque, NamedTuple, Tuple
4
7
 
5
8
  import pytest
6
9
 
7
- from crosshair.core import deep_realize, proxy_for_type, realize, standalone_statespace
10
+ from crosshair.core import (
11
+ deep_realize,
12
+ get_constructor_signature,
13
+ proxy_for_type,
14
+ realize,
15
+ standalone_statespace,
16
+ )
8
17
  from crosshair.libimpl.collectionslib import ListBasedDeque
9
18
  from crosshair.statespace import CANNOT_CONFIRM, CONFIRMED, POST_FAIL, MessageType
10
19
  from crosshair.test_util import check_states
11
20
  from crosshair.tracers import NoTracing, ResumedTracing
21
+ from crosshair.util import CrossHairValue
12
22
 
13
23
 
14
24
  @pytest.fixture
@@ -24,7 +34,7 @@ def test_counter_symbolic_deep(space):
24
34
 
25
35
 
26
36
  def test_counter_deep(space):
27
- d = collections.Counter()
37
+ d = Counter()
28
38
  with ResumedTracing():
29
39
  deep_realize(d)
30
40
  deepcopy(d)
@@ -89,7 +99,11 @@ def test_deque_index_with_start_index_throws_correct_exception(test_list) -> Non
89
99
  with pytest.raises(ValueError) as context:
90
100
  test_list.index(1, 2)
91
101
 
92
- assert context.match("1 is not in list")
102
+ if sys.version_info >= (3, 14):
103
+ # assert context.match(re.escape("list.index(x): x not in list"))
104
+ assert context.match(re.escape("deque.index(x): x not in deque"))
105
+ else:
106
+ assert context.match("1 is not in list")
93
107
 
94
108
 
95
109
  def test_deque_index_with_start_and_end_index(test_list) -> None:
@@ -103,7 +117,10 @@ def test_deque_index_with_start_and_end_index_throws_correct_exception(
103
117
  with pytest.raises(ValueError) as context:
104
118
  test_list.index(6, 0, 1)
105
119
 
106
- assert context.match("6 is not in list")
120
+ if sys.version_info >= (3, 14):
121
+ assert context.match(re.escape("deque.index(x): x not in deque"))
122
+ else:
123
+ assert context.match("6 is not in list")
107
124
 
108
125
 
109
126
  def test_deque_insert(test_list) -> None:
@@ -176,7 +193,7 @@ def test_deque_extendleft_method() -> None:
176
193
  """
177
194
  Can any deque be extended by itself and form this palindrome?
178
195
 
179
- post[ls]: ls != collections.deque([1, 2, 3, 3, 2, 1])
196
+ post[ls]: ls != deque([1, 2, 3, 3, 2, 1])
180
197
  """
181
198
  ls.extendleft(ls)
182
199
 
@@ -185,22 +202,22 @@ def test_deque_extendleft_method() -> None:
185
202
 
186
203
  def test_deque_add_symbolic_to_concrete():
187
204
  with standalone_statespace as space:
188
- d = ListBasedDeque([1, 2]) + collections.deque([3, 4])
205
+ d = ListBasedDeque([1, 2]) + deque([3, 4])
189
206
  assert list(d) == [1, 2, 3, 4]
190
207
 
191
208
 
192
209
  def test_deque_eq():
193
210
  with standalone_statespace as space:
194
211
  assert ListBasedDeque([1, 2]) == ListBasedDeque([1, 2])
195
- assert collections.deque([1, 2]) == ListBasedDeque([1, 2])
212
+ assert deque([1, 2]) == ListBasedDeque([1, 2])
196
213
  assert ListBasedDeque([1, 2]) != ListBasedDeque([1, 55])
197
- assert collections.deque([1, 2]) != ListBasedDeque([1, 55])
214
+ assert deque([1, 2]) != ListBasedDeque([1, 55])
198
215
 
199
216
 
200
217
  def test_defaultdict_repr_equiv(test_list) -> None:
201
218
  def f(symbolic: DefaultDict[int, int]) -> Tuple[dict, dict]:
202
219
  """post: _[0] == _[1]"""
203
- concrete = collections.defaultdict(symbolic.default_factory, symbolic.items())
220
+ concrete = defaultdict(symbolic.default_factory, symbolic.items())
204
221
  return (symbolic, concrete)
205
222
 
206
223
  check_states(f, CANNOT_CONFIRM)
@@ -243,16 +260,73 @@ def test_defaultdict_realize():
243
260
  with standalone_statespace:
244
261
  with NoTracing():
245
262
  d = proxy_for_type(DefaultDict[int, int], "d")
246
- assert type(realize(d)) is collections.defaultdict
263
+ assert type(realize(d)) is defaultdict
247
264
 
248
265
 
249
266
  #
250
- # We don't patch namedtuple, but namedtuple performs magic like dynamic type
267
+ # We don't patch namedtuple, but namedtuple performs magic dynamic type
251
268
  # generation, which can interfere with CrossHair.
252
269
  #
253
270
 
254
271
 
255
272
  def test_namedtuple_creation():
256
273
  with standalone_statespace:
257
- # Ensure type creation doesn't raise exception:
258
- Color = collections.namedtuple("Color", ("name", "hex"))
274
+ # Ensure type creation under trace doesn't raise exception:
275
+ Color = namedtuple("Color", ("name", "hex"))
276
+
277
+
278
+ def test_namedtuple_argument_detection_untyped():
279
+ UntypedColor = namedtuple("UntypedColor", ("name", "hex"))
280
+ expected_signature = Signature(
281
+ parameters=[
282
+ Parameter("name", Parameter.POSITIONAL_OR_KEYWORD),
283
+ Parameter("hex", Parameter.POSITIONAL_OR_KEYWORD),
284
+ ],
285
+ return_annotation=Signature.empty,
286
+ )
287
+ assert get_constructor_signature(UntypedColor) == expected_signature
288
+
289
+
290
+ def test_namedtuple_argument_detection_typed_with_subclass():
291
+ class ClassTypedColor(NamedTuple):
292
+ name: str
293
+ hex: int
294
+
295
+ expected_parameters = {
296
+ "name": Parameter("name", Parameter.POSITIONAL_OR_KEYWORD, annotation=str),
297
+ "hex": Parameter("hex", Parameter.POSITIONAL_OR_KEYWORD, annotation=int),
298
+ }
299
+ assert get_constructor_signature(ClassTypedColor).parameters == expected_parameters
300
+
301
+
302
+ @pytest.mark.skipif(
303
+ sys.version_info < (3, 9),
304
+ reason="Functional namedtuple field types supported on Python >= 3.9",
305
+ )
306
+ def test_namedtuple_argument_detection_typed_functionally():
307
+ FunctionallyTypedColor = NamedTuple(
308
+ "FunctionallyTypedColor", [("name", str), ("hex", int)]
309
+ )
310
+ expected_parameters = {
311
+ "name": Parameter("name", Parameter.POSITIONAL_OR_KEYWORD, annotation=str),
312
+ "hex": Parameter("hex", Parameter.POSITIONAL_OR_KEYWORD, annotation=int),
313
+ }
314
+ assert (
315
+ get_constructor_signature(FunctionallyTypedColor).parameters
316
+ == expected_parameters
317
+ )
318
+
319
+
320
+ @pytest.mark.skipif(
321
+ sys.version_info < (3, 9),
322
+ reason="Functional namedtuple field types supported on Python >= 3.9",
323
+ )
324
+ def test_namedtuple_symbolic_creation(space):
325
+ UntypedColor = namedtuple("Color", "name hex")
326
+ Color = NamedTuple("Color", [("name", str), ("hex", int)])
327
+ untyped_color = proxy_for_type(UntypedColor, "color")
328
+ assert isinstance(untyped_color.hex, CrossHairValue)
329
+ color = proxy_for_type(Color, "color")
330
+ with ResumedTracing():
331
+ assert space.is_possible(color.hex == 5)
332
+ assert space.is_possible(color.hex == 10)
@@ -1,7 +1,12 @@
1
1
  import codecs
2
- from collections.abc import ByteString
2
+ import sys
3
3
  from dataclasses import dataclass
4
- from typing import Dict, List, Optional, Tuple, Type, Union
4
+ from typing import List, Optional, Tuple, Type, Union
5
+
6
+ if sys.version_info >= (3, 12):
7
+ from collections.abc import Buffer
8
+ else:
9
+ from collections.abc import ByteString as Buffer
5
10
 
6
11
  from crosshair.core import realize
7
12
  from crosshair.libimpl.builtinslib import AnySymbolicStr, SymbolicBytes
@@ -87,7 +92,7 @@ class StemEncoder:
87
92
  def decode(
88
93
  cls, input: bytes, errors: str = "strict"
89
94
  ) -> Tuple[Union[str, AnySymbolicStr], int]:
90
- if not (isinstance(input, ByteString) and isinstance(errors, str)):
95
+ if not (isinstance(input, Buffer) and isinstance(errors, str)):
91
96
  raise TypeError
92
97
  parts: List[Union[str, AnySymbolicStr]] = []
93
98
  idx = 0
@@ -1,6 +1,5 @@
1
1
  import math
2
2
  import sys
3
- import unittest
4
3
 
5
4
  import pytest
6
5
 
@@ -66,9 +65,3 @@ def test_log():
66
65
  space.add(f > 0)
67
66
  math.log(i)
68
67
  math.log(f)
69
-
70
-
71
- if __name__ == "__main__":
72
- if ("-v" in sys.argv) or ("--verbose" in sys.argv):
73
- set_debug(True)
74
- unittest.main()
@@ -122,7 +122,7 @@ def check_search_anchored_end(text: str, flags: int) -> ResultComparison:
122
122
 
123
123
  def check_subn(text: str, flags: int) -> ResultComparison:
124
124
  """post: _"""
125
- return compare_results(lambda t, f: re.subn("aa", "ba", t, f), text, flags)
125
+ return compare_results(lambda t, f: re.subn("aa", "ba", t, flags=f), text, flags)
126
126
 
127
127
 
128
128
  def check_lookahead(text: str) -> ResultComparison:
@@ -150,7 +150,7 @@ def check_negative_lookbehind(text: str) -> ResultComparison:
150
150
 
151
151
  def check_subn_bytes(text: bytes, flags: int) -> ResultComparison:
152
152
  """post: _"""
153
- return compare_results(lambda t, f: re.subn(b"a", b"b", t, f), text, flags)
153
+ return compare_results(lambda t, f: re.subn(b"a", b"b", t, flags=f), text, flags)
154
154
 
155
155
 
156
156
  def check_findall_bytes(text: bytes, flags: int) -> ResultComparison:
@@ -1,27 +1,45 @@
1
1
  import time as real_time
2
2
  from inspect import Signature
3
3
  from math import isfinite
4
- from typing import Any, Callable
4
+ from typing import Any, Literal
5
5
 
6
- from crosshair.core import FunctionInterps
6
+ from crosshair.core import register_patch
7
7
  from crosshair.register_contract import register_contract
8
8
  from crosshair.statespace import context_statespace
9
9
  from crosshair.tracers import NoTracing
10
10
 
11
11
 
12
- def _gte_last(fn: Callable, value: Any) -> bool:
12
+ class EarliestPossibleTime:
13
+ monotonic: float = 0.0
14
+ process_time: float = 0.0
15
+
16
+ def __init__(self, *a):
17
+ pass
18
+
19
+
20
+ # Imprecision at high values becomes a sort of artificial problem
21
+ _UNREALISTICALLY_LARGE_TIME_FLOAT = float(60 * 60 * 24 * 365 * 100_000)
22
+
23
+
24
+ def _gte_last(kind: Literal["monotonic", "process_time"], value: Any) -> bool:
25
+ with NoTracing():
26
+ earliest_times = context_statespace().extra(EarliestPossibleTime)
27
+ threshold = getattr(earliest_times, kind)
28
+ setattr(earliest_times, kind, value)
29
+ return all([threshold <= value, value < _UNREALISTICALLY_LARGE_TIME_FLOAT])
30
+
31
+
32
+ def _sleep(value: float) -> None:
13
33
  with NoTracing():
14
- interps = context_statespace().extra(FunctionInterps)
15
- previous = interps._interpretations[fn]
16
- if len(previous) < 2:
17
- return True
18
- return value >= previous[-2]
34
+ earliest_times = context_statespace().extra(EarliestPossibleTime)
35
+ earliest_times.monotonic += value
36
+ return None
19
37
 
20
38
 
21
39
  def make_registrations():
22
40
  register_contract(
23
41
  real_time.time,
24
- post=lambda __return__: __return__ > 0.0,
42
+ post=lambda __return__: __return__ > 0.0 and isfinite(__return__),
25
43
  sig=Signature(parameters=[], return_annotation=float),
26
44
  )
27
45
  register_contract(
@@ -31,23 +49,24 @@ def make_registrations():
31
49
  )
32
50
  register_contract(
33
51
  real_time.monotonic,
34
- post=lambda __return__: isfinite(__return__)
35
- and _gte_last(real_time.monotonic, __return__),
52
+ post=lambda __return__: _gte_last("monotonic", __return__)
53
+ and isfinite(__return__),
36
54
  sig=Signature(parameters=[], return_annotation=float),
37
55
  )
38
56
  register_contract(
39
57
  real_time.monotonic_ns,
40
- post=lambda __return__: isfinite(__return__)
41
- and _gte_last(real_time.monotonic_ns, __return__),
58
+ post=lambda __return__: _gte_last("monotonic", __return__ / 1_000_000_000),
42
59
  sig=Signature(parameters=[], return_annotation=int),
43
60
  )
44
61
  register_contract(
45
62
  real_time.process_time,
46
- post=lambda __return__: _gte_last(real_time.process_time, __return__),
63
+ post=lambda __return__: _gte_last("process_time", __return__)
64
+ and isfinite(__return__),
47
65
  sig=Signature(parameters=[], return_annotation=float),
48
66
  )
49
67
  register_contract(
50
68
  real_time.process_time_ns,
51
- post=lambda __return__: _gte_last(real_time.process_time_ns, __return__),
69
+ post=lambda __return__: _gte_last("process_time", __return__ / 1_000_000_000),
52
70
  sig=Signature(parameters=[], return_annotation=int),
53
71
  )
72
+ register_patch(real_time.sleep, _sleep)
@@ -2,7 +2,7 @@ import time
2
2
 
3
3
  import pytest
4
4
 
5
- from crosshair.statespace import CONFIRMED, POST_FAIL
5
+ from crosshair.statespace import CANNOT_CONFIRM, CONFIRMED, POST_FAIL
6
6
  from crosshair.test_util import check_states
7
7
 
8
8
 
@@ -69,4 +69,14 @@ def test_monotonic_ns():
69
69
  start = time.monotonic_ns()
70
70
  return time.monotonic_ns() - start
71
71
 
72
- check_states(f, CONFIRMED)
72
+ check_states(f, CANNOT_CONFIRM)
73
+
74
+
75
+ def test_sleep():
76
+ def f():
77
+ """post: _ >= 60.0"""
78
+ start = time.monotonic()
79
+ time.sleep(60.01)
80
+ return time.monotonic() - start
81
+
82
+ check_states(f, CANNOT_CONFIRM)
crosshair/lsp_server.py CHANGED
@@ -86,7 +86,7 @@ def publish_messages(
86
86
  if message.state < MessageType.PRE_UNSAT:
87
87
  continue
88
88
  # TODO: consider server.show_message_log()ing the long description
89
- diagnostics.append(get_diagnostic(message, doc.lines if doc else ()))
89
+ diagnostics.append(get_diagnostic(message, doc.lines if doc else []))
90
90
  server.publish_diagnostics(uri, diagnostics)
91
91
  if not diagnostics:
92
92
  # After we publish an empty set, it's safe to forget about the file:
crosshair/main.py CHANGED
@@ -448,7 +448,9 @@ def run_watch_loop(
448
448
  active_messages = {}
449
449
  else:
450
450
  time.sleep(0.1)
451
- max_uninteresting_iterations *= 2
451
+ max_uninteresting_iterations = min(
452
+ max_uninteresting_iterations * 2, 100_000_000
453
+ )
452
454
  for curstats, messages in watcher.run_iteration(max_uninteresting_iterations):
453
455
  messages = [m for m in messages if m.state > MessageType.PRE_UNSAT]
454
456
  stats.update(curstats)
@@ -1,4 +1,4 @@
1
- import unittest
1
+ import pytest
2
2
 
3
3
  from crosshair.objectproxy import ObjectProxy
4
4
 
@@ -11,17 +11,13 @@ class ObjectWrap(ObjectProxy):
11
11
  return object.__getattribute__(self, "_o")
12
12
 
13
13
 
14
- class ObjectProxyTest(unittest.TestCase):
14
+ class TestObjectProxy:
15
15
  def test_object_proxy(self) -> None:
16
16
  i = [1, 2, 3]
17
17
  proxy = ObjectWrap(i)
18
- self.assertEqual(i, proxy)
18
+ assert i == proxy
19
19
  proxy.append(4)
20
- self.assertEqual([1, 2, 3, 4], proxy)
21
- self.assertEqual([1, 2, 3, 4, 5], proxy + [5])
22
- self.assertEqual([2, 3], proxy[1:3])
23
- self.assertEqual([1, 2, 3, 4], proxy)
24
-
25
-
26
- if __name__ == "__main__":
27
- unittest.main()
20
+ assert [1, 2, 3, 4] == proxy
21
+ assert [1, 2, 3, 4, 5] == proxy + [5]
22
+ assert [2, 3] == proxy[1:3]
23
+ assert [1, 2, 3, 4] == proxy
@@ -26,10 +26,10 @@ from crosshair.tracers import (
26
26
  frame_stack_read,
27
27
  frame_stack_write,
28
28
  )
29
- from crosshair.util import CrossHairInternal, CrossHairValue
29
+ from crosshair.util import CROSSHAIR_EXTRA_ASSERTS, CrossHairInternal, CrossHairValue
30
30
  from crosshair.z3util import z3Not, z3Or
31
31
 
32
- BINARY_SUBSCR = dis.opmap["BINARY_SUBSCR"]
32
+ BINARY_SUBSCR = dis.opmap.get("BINARY_SUBSCR", 256)
33
33
  BINARY_SLICE = dis.opmap.get("BINARY_SLICE", 256)
34
34
  BUILD_STRING = dis.opmap["BUILD_STRING"]
35
35
  COMPARE_OP = dis.opmap["COMPARE_OP"]
@@ -61,11 +61,17 @@ _DEEPLY_CONCRETE_KEY_TYPES = (
61
61
 
62
62
 
63
63
  class SymbolicSubscriptInterceptor(TracingModule):
64
- opcodes_wanted = frozenset([BINARY_SUBSCR])
64
+ opcodes_wanted = frozenset([BINARY_SUBSCR, BINARY_OP])
65
65
 
66
66
  def trace_op(self, frame, codeobj, codenum):
67
67
  # Note that because this is called from inside a Python trace handler, tracing
68
68
  # is automatically disabled, so there's no need for a `with NoTracing():` guard.
69
+
70
+ if codenum == BINARY_OP:
71
+ oparg = frame_op_arg(frame)
72
+ if oparg != 26: # subscript operator, NB_SUBSCR
73
+ return
74
+
69
75
  key = frame_stack_read(frame, -1)
70
76
  if isinstance(key, _DEEPLY_CONCRETE_KEY_TYPES):
71
77
  return
@@ -284,6 +290,7 @@ class BuildStringInterceptor(TracingModule):
284
290
  class FormatValueInterceptor(TracingModule):
285
291
  """Avoid checks and realization during FORMAT_VALUE (used by f-strings)."""
286
292
 
293
+ # TODO: don't we need to handle FORMAT_SIMPLE and FORMAT_WITH_SPEC?
287
294
  opcodes_wanted = frozenset([FORMAT_VALUE, CONVERT_VALUE])
288
295
 
289
296
  def trace_op(self, frame, codeobj, codenum):
@@ -344,7 +351,9 @@ class MapAddInterceptor(TracingModule):
344
351
  # Afterwards, overwrite the interpreter's resulting dict with ours:
345
352
  def post_op():
346
353
  old_dict_obj = frame_stack_read(frame, dict_offset + 2)
347
- if not isinstance(old_dict_obj, (dict, MutableMapping)):
354
+ if CROSSHAIR_EXTRA_ASSERTS and not isinstance(
355
+ old_dict_obj, (dict, MutableMapping)
356
+ ):
348
357
  raise CrossHairInternal("interpreter stack corruption detected")
349
358
  frame_stack_write(frame, dict_offset + 2, dict_obj)
350
359
 
@@ -426,7 +435,8 @@ class SetAddInterceptor(TracingModule):
426
435
  # Set and value are concrete; continue as normal.
427
436
  return
428
437
  # Have the interpreter do a fake addition, namely `set().add(1)`
429
- frame_stack_write(frame, set_offset, set())
438
+ dummy_set: Set = set()
439
+ frame_stack_write(frame, set_offset, dummy_set)
430
440
  frame_stack_write(frame, -1, 1)
431
441
 
432
442
  # And do our own addition separately:
@@ -434,6 +444,12 @@ class SetAddInterceptor(TracingModule):
434
444
 
435
445
  # Later, overwrite the interpreter's result with ours:
436
446
  def post_op():
447
+ if CROSSHAIR_EXTRA_ASSERTS:
448
+ to_replace = frame_stack_read(frame, set_offset + 1)
449
+ if to_replace is not dummy_set:
450
+ raise CrossHairInternal(
451
+ f"Found an instance of {type(to_replace)} where dummy set should be."
452
+ )
437
453
  frame_stack_write(frame, set_offset + 1, set_obj)
438
454
 
439
455
  COMPOSITE_TRACER.set_postop_callback(post_op, frame)
@@ -449,9 +465,9 @@ class IdentityInterceptor(TracingModule):
449
465
  def trace_op(self, frame: FrameType, codeobj: CodeType, codenum: int) -> None:
450
466
  arg1 = frame_stack_read(frame, -1)
451
467
  arg2 = frame_stack_read(frame, -2)
452
- if isinstance(arg1, SymbolicBool):
468
+ if isinstance(arg1, SymbolicBool) and isinstance(arg2, (bool, SymbolicBool)):
453
469
  frame_stack_write(frame, -1, arg1.__ch_realize__())
454
- if isinstance(arg2, SymbolicBool):
470
+ if isinstance(arg2, SymbolicBool) and isinstance(arg1, (bool, SymbolicBool)):
455
471
  frame_stack_write(frame, -2, arg2.__ch_realize__())
456
472
 
457
473
 
@@ -466,7 +482,7 @@ class ModuloInterceptor(TracingModule):
466
482
  if isinstance(left, str):
467
483
  if codenum == BINARY_OP:
468
484
  oparg = frame_op_arg(frame)
469
- if oparg != 6: # modulo operator (determined experimentally)
485
+ if oparg != 6: # modulo operator, NB_REMAINDER
470
486
  return
471
487
  frame_stack_write(frame, -2, DeoptimizedPercentFormattingStr(left))
472
488
 
@@ -60,7 +60,7 @@ def test_dict_key_containment():
60
60
  check_states(numstr, POST_FAIL)
61
61
 
62
62
 
63
- def test_dict_comprehension():
63
+ def test_dict_comprehension_basic():
64
64
  with standalone_statespace as space:
65
65
  with NoTracing():
66
66
  x = proxy_for_type(int, "x")
@@ -139,7 +139,7 @@ def test_not_operator_on_non_bool():
139
139
  assert notList
140
140
 
141
141
 
142
- def test_set_comprehension():
142
+ def test_set_comprehension_basic():
143
143
  with standalone_statespace as space:
144
144
  with NoTracing():
145
145
  x = proxy_for_type(int, "x")
@@ -195,3 +195,14 @@ def test_identity_operator_on_booleans():
195
195
  b1 = proxy_for_type(bool, "b1")
196
196
  space.add(b1)
197
197
  assert b1 is True
198
+
199
+
200
+ @pytest.mark.skipif(sys.version_info < (3, 9), reason="IS_OP is new in Python 3.9")
201
+ def test_identity_operator_does_not_realize_on_differing_types():
202
+ with standalone_statespace as space:
203
+ with NoTracing():
204
+ b1 = proxy_for_type(bool, "b1")
205
+ choices_made_at_start = len(space.choices_made)
206
+ space.add(b1)
207
+ _ = b1 is 42 # noqa: F632
208
+ assert len(space.choices_made) == choices_made_at_start
crosshair/py.typed ADDED
File without changes
crosshair/tracers.py CHANGED
@@ -132,6 +132,14 @@ def handle_call_function_ex_3_13(frame) -> CallStackInfo:
132
132
  return (idx, NULL_POINTER, kwargs_idx) # type: ignore
133
133
 
134
134
 
135
+ def handle_call_function_ex_3_14(frame) -> CallStackInfo:
136
+ callable_idx, kwargs_idx = -4, -1
137
+ try:
138
+ return (callable_idx, frame_stack_read(frame, callable_idx), kwargs_idx)
139
+ except ValueError:
140
+ return (callable_idx, NULL_POINTER, kwargs_idx) # type: ignore
141
+
142
+
135
143
  def handle_call_method(frame) -> CallStackInfo:
136
144
  idx = -(frame.f_code.co_code[frame.f_lasti + 1] + 2)
137
145
  try:
@@ -148,9 +156,13 @@ _CALL_HANDLERS: Dict[int, Callable[[object], CallStackInfo]] = {
148
156
  CALL_KW: handle_call_kw,
149
157
  CALL_FUNCTION: handle_call_function,
150
158
  CALL_FUNCTION_KW: handle_call_function_kw,
151
- CALL_FUNCTION_EX: handle_call_function_ex_3_13
152
- if sys.version_info >= (3, 13)
153
- else handle_call_function_ex_3_6,
159
+ CALL_FUNCTION_EX: handle_call_function_ex_3_14
160
+ if sys.version_info >= (3, 14)
161
+ else (
162
+ handle_call_function_ex_3_13
163
+ if sys.version_info >= (3, 13)
164
+ else handle_call_function_ex_3_6
165
+ ),
154
166
  CALL_METHOD: handle_call_method,
155
167
  }
156
168
 
@@ -236,12 +248,18 @@ class TracingModule:
236
248
  target = __func
237
249
 
238
250
  if kwargs_idx is not None:
239
- kwargs_dict = frame_stack_read(frame, kwargs_idx)
240
- replacement_kwargs = {
241
- key.__ch_realize__() if hasattr(key, "__ch_realize__") else key: val
242
- for key, val in kwargs_dict.items()
243
- }
244
- frame_stack_write(frame, kwargs_idx, replacement_kwargs)
251
+ try:
252
+ kwargs_dict = frame_stack_read(frame, kwargs_idx)
253
+ except ValueError:
254
+ pass
255
+ else:
256
+ replacement_kwargs = {
257
+ # TODO: I don't think it's safe to realize in the middle of a tracing operation.
258
+ # Need to confirm with test. I guess we have to wrap the callable instead?
259
+ key.__ch_realize__() if hasattr(key, "__ch_realize__") else key: val
260
+ for key, val in kwargs_dict.items()
261
+ }
262
+ frame_stack_write(frame, kwargs_idx, replacement_kwargs)
245
263
 
246
264
  if isinstance(target, Untracable):
247
265
  return None
crosshair/type_repo.py CHANGED
@@ -6,7 +6,7 @@ from typing import Dict, List, Optional, Type
6
6
  import z3 # type: ignore
7
7
 
8
8
  from crosshair.tracers import NoTracing
9
- from crosshair.util import CrossHairInternal, CrossHairValue
9
+ from crosshair.util import CrossHairInternal, CrossHairValue, is_hashable
10
10
  from crosshair.z3util import z3Eq, z3Not
11
11
 
12
12
  _MAP: Optional[Dict[type, List[type]]] = None
@@ -67,7 +67,7 @@ def get_subclass_map() -> Dict[type, List[type]]:
67
67
  except ImportError:
68
68
  continue
69
69
  for _, cls in module_classes:
70
- if _class_known_to_be_copyable(cls):
70
+ if _class_known_to_be_copyable(cls) and is_hashable(cls):
71
71
  classes.add(cls)
72
72
  subclass = collections.defaultdict(list)
73
73
  for cls in classes: