crosshair-tool 0.0.83__cp312-cp312-win32.whl → 0.0.85__cp312-cp312-win32.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.
- _crosshair_tracers.cp312-win32.pyd +0 -0
- crosshair/__init__.py +1 -1
- crosshair/_mark_stacks.h +0 -25
- crosshair/_tracers.h +2 -0
- crosshair/_tracers_test.py +8 -2
- crosshair/auditwall.py +0 -1
- crosshair/auditwall_test.py +5 -0
- crosshair/condition_parser.py +5 -5
- crosshair/condition_parser_test.py +50 -63
- crosshair/copyext.py +23 -7
- crosshair/copyext_test.py +11 -1
- crosshair/core.py +23 -17
- crosshair/core_test.py +625 -584
- crosshair/diff_behavior_test.py +14 -21
- crosshair/dynamic_typing.py +90 -1
- crosshair/dynamic_typing_test.py +73 -1
- crosshair/enforce_test.py +15 -22
- crosshair/fnutil_test.py +4 -8
- crosshair/libimpl/arraylib.py +2 -5
- crosshair/libimpl/binasciilib.py +2 -3
- crosshair/libimpl/builtinslib.py +28 -21
- crosshair/libimpl/builtinslib_test.py +1 -8
- crosshair/libimpl/collectionslib.py +18 -3
- crosshair/libimpl/collectionslib_test.py +89 -15
- crosshair/libimpl/encodings/_encutil.py +8 -3
- crosshair/libimpl/mathlib_test.py +0 -7
- crosshair/libimpl/relib_ch_test.py +2 -2
- crosshair/libimpl/timelib.py +34 -15
- crosshair/libimpl/timelib_test.py +12 -2
- crosshair/lsp_server.py +1 -1
- crosshair/main.py +3 -1
- crosshair/objectproxy_test.py +7 -11
- crosshair/opcode_intercept.py +24 -8
- crosshair/opcode_intercept_test.py +13 -2
- crosshair/py.typed +0 -0
- crosshair/tracers.py +27 -9
- crosshair/type_repo.py +2 -2
- crosshair/unicode_categories.py +1 -0
- crosshair/util.py +45 -16
- crosshair/watcher.py +2 -2
- {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/METADATA +4 -3
- {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/RECORD +46 -45
- {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/WHEEL +1 -1
- {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info}/entry_points.txt +0 -0
- {crosshair_tool-0.0.83.dist-info → crosshair_tool-0.0.85.dist-info/licenses}/LICENSE +0 -0
- {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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
|
1
|
+
import re
|
|
2
|
+
import sys
|
|
3
|
+
from collections import Counter, defaultdict, deque, namedtuple
|
|
2
4
|
from copy import deepcopy
|
|
3
|
-
from
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 !=
|
|
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]) +
|
|
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
|
|
212
|
+
assert deque([1, 2]) == ListBasedDeque([1, 2])
|
|
196
213
|
assert ListBasedDeque([1, 2]) != ListBasedDeque([1, 55])
|
|
197
|
-
assert
|
|
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 =
|
|
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
|
|
263
|
+
assert type(realize(d)) is defaultdict
|
|
247
264
|
|
|
248
265
|
|
|
249
266
|
#
|
|
250
|
-
# We don't patch namedtuple, but namedtuple performs magic
|
|
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 =
|
|
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
|
-
|
|
2
|
+
import sys
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import
|
|
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,
|
|
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:
|
crosshair/libimpl/timelib.py
CHANGED
|
@@ -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,
|
|
4
|
+
from typing import Any, Literal
|
|
5
5
|
|
|
6
|
-
from crosshair.core import
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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__:
|
|
35
|
-
and
|
|
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__:
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
|
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)
|
crosshair/objectproxy_test.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
|
14
|
+
class TestObjectProxy:
|
|
15
15
|
def test_object_proxy(self) -> None:
|
|
16
16
|
i = [1, 2, 3]
|
|
17
17
|
proxy = ObjectWrap(i)
|
|
18
|
-
|
|
18
|
+
assert i == proxy
|
|
19
19
|
proxy.append(4)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
crosshair/opcode_intercept.py
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
152
|
-
if sys.version_info >= (3,
|
|
153
|
-
else
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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:
|