shrinkray 0.0.0__py3-none-any.whl → 25.12.26.0__py3-none-any.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.
- shrinkray/__main__.py +130 -960
- shrinkray/cli.py +70 -0
- shrinkray/display.py +75 -0
- shrinkray/formatting.py +108 -0
- shrinkray/passes/bytes.py +217 -10
- shrinkray/passes/clangdelta.py +47 -17
- shrinkray/passes/definitions.py +84 -4
- shrinkray/passes/genericlanguages.py +61 -7
- shrinkray/passes/json.py +6 -0
- shrinkray/passes/patching.py +65 -57
- shrinkray/passes/python.py +66 -23
- shrinkray/passes/sat.py +505 -91
- shrinkray/passes/sequences.py +26 -6
- shrinkray/problem.py +206 -27
- shrinkray/process.py +49 -0
- shrinkray/reducer.py +187 -25
- shrinkray/state.py +599 -0
- shrinkray/subprocess/__init__.py +24 -0
- shrinkray/subprocess/client.py +253 -0
- shrinkray/subprocess/protocol.py +190 -0
- shrinkray/subprocess/worker.py +491 -0
- shrinkray/tui.py +915 -0
- shrinkray/ui.py +72 -0
- shrinkray/work.py +34 -6
- {shrinkray-0.0.0.dist-info → shrinkray-25.12.26.0.dist-info}/METADATA +44 -27
- shrinkray-25.12.26.0.dist-info/RECORD +33 -0
- {shrinkray-0.0.0.dist-info → shrinkray-25.12.26.0.dist-info}/WHEEL +2 -1
- shrinkray-25.12.26.0.dist-info/entry_points.txt +3 -0
- shrinkray-25.12.26.0.dist-info/top_level.txt +1 -0
- shrinkray/learning.py +0 -221
- shrinkray-0.0.0.dist-info/RECORD +0 -22
- shrinkray-0.0.0.dist-info/entry_points.txt +0 -3
- {shrinkray-0.0.0.dist-info → shrinkray-25.12.26.0.dist-info/licenses}/LICENSE +0 -0
shrinkray/reducer.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from collections.abc import Generator
|
|
2
|
+
from collections.abc import Generator, Iterable
|
|
3
3
|
from contextlib import contextmanager
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any
|
|
5
5
|
|
|
6
6
|
import attrs
|
|
7
7
|
import trio
|
|
@@ -15,6 +15,7 @@ from shrinkray.passes.bytes import (
|
|
|
15
15
|
hollow,
|
|
16
16
|
lexeme_based_deletions,
|
|
17
17
|
lift_braces,
|
|
18
|
+
line_sorter,
|
|
18
19
|
lower_bytes,
|
|
19
20
|
lower_individual_bytes,
|
|
20
21
|
remove_indents,
|
|
@@ -23,8 +24,17 @@ from shrinkray.passes.bytes import (
|
|
|
23
24
|
short_deletions,
|
|
24
25
|
standard_substitutions,
|
|
25
26
|
)
|
|
26
|
-
from shrinkray.passes.clangdelta import
|
|
27
|
-
|
|
27
|
+
from shrinkray.passes.clangdelta import (
|
|
28
|
+
C_FILE_EXTENSIONS,
|
|
29
|
+
ClangDelta,
|
|
30
|
+
clang_delta_pumps,
|
|
31
|
+
)
|
|
32
|
+
from shrinkray.passes.definitions import (
|
|
33
|
+
Format,
|
|
34
|
+
ReductionPass,
|
|
35
|
+
ReductionPump,
|
|
36
|
+
compose,
|
|
37
|
+
)
|
|
28
38
|
from shrinkray.passes.genericlanguages import (
|
|
29
39
|
combine_expressions,
|
|
30
40
|
cut_comment_like_things,
|
|
@@ -39,16 +49,18 @@ from shrinkray.passes.patching import PatchApplier, Patches
|
|
|
39
49
|
from shrinkray.passes.python import PYTHON_PASSES, is_python
|
|
40
50
|
from shrinkray.passes.sat import SAT_PASSES, DimacsCNF
|
|
41
51
|
from shrinkray.passes.sequences import block_deletion, delete_duplicates
|
|
42
|
-
from shrinkray.problem import ReductionProblem, shortlex
|
|
43
|
-
|
|
44
|
-
S = TypeVar("S")
|
|
45
|
-
T = TypeVar("T")
|
|
52
|
+
from shrinkray.problem import ReductionProblem, ReductionStats, shortlex
|
|
46
53
|
|
|
47
54
|
|
|
48
55
|
@define
|
|
49
|
-
class Reducer
|
|
56
|
+
class Reducer[T](ABC):
|
|
50
57
|
target: ReductionProblem[T]
|
|
51
58
|
|
|
59
|
+
# Optional pass statistics tracking (implemented by ShrinkRay)
|
|
60
|
+
pass_stats: "PassStatsTracker | None" = attrs.field(default=None, init=False)
|
|
61
|
+
# Optional current pass tracking (implemented by ShrinkRay)
|
|
62
|
+
current_reduction_pass: "ReductionPass[T] | None" = attrs.field(default=None, init=False)
|
|
63
|
+
|
|
52
64
|
@contextmanager
|
|
53
65
|
def backtrack(self, restart: T) -> Generator[None, None, None]:
|
|
54
66
|
current = self.target
|
|
@@ -65,16 +77,38 @@ class Reducer(Generic[T], ABC):
|
|
|
65
77
|
def status(self) -> str:
|
|
66
78
|
return ""
|
|
67
79
|
|
|
80
|
+
@property
|
|
81
|
+
def disabled_passes(self) -> set[str]:
|
|
82
|
+
"""Set of disabled pass names. Override in subclasses for pass control."""
|
|
83
|
+
return set()
|
|
84
|
+
|
|
85
|
+
def disable_pass(self, pass_name: str) -> None: # noqa: B027
|
|
86
|
+
"""Disable a pass by name. Override in subclasses for pass control."""
|
|
87
|
+
|
|
88
|
+
def enable_pass(self, pass_name: str) -> None: # noqa: B027
|
|
89
|
+
"""Enable a pass by name. Override in subclasses for pass control."""
|
|
90
|
+
|
|
91
|
+
def skip_current_pass(self) -> None: # noqa: B027
|
|
92
|
+
"""Skip the currently running pass. Override in subclasses for pass control."""
|
|
93
|
+
|
|
68
94
|
|
|
69
95
|
@define
|
|
70
|
-
class BasicReducer(Reducer[T]):
|
|
96
|
+
class BasicReducer[T](Reducer[T]):
|
|
71
97
|
reduction_passes: Iterable[ReductionPass[T]]
|
|
72
98
|
pumps: Iterable[ReductionPump[T]] = ()
|
|
73
|
-
|
|
99
|
+
_status: str = "Starting up"
|
|
74
100
|
|
|
75
101
|
def __attrs_post_init__(self) -> None:
|
|
76
102
|
self.reduction_passes = list(self.reduction_passes)
|
|
77
103
|
|
|
104
|
+
@property
|
|
105
|
+
def status(self) -> str:
|
|
106
|
+
return self._status
|
|
107
|
+
|
|
108
|
+
@status.setter
|
|
109
|
+
def status(self, value: str) -> None:
|
|
110
|
+
self._status = value
|
|
111
|
+
|
|
78
112
|
async def run_pass(self, rp: ReductionPass[T]) -> None:
|
|
79
113
|
await rp(self.target)
|
|
80
114
|
|
|
@@ -102,14 +136,88 @@ class RestartPass(Exception):
|
|
|
102
136
|
pass
|
|
103
137
|
|
|
104
138
|
|
|
139
|
+
@define
|
|
140
|
+
class PassStats:
|
|
141
|
+
"""Statistics for a single reduction pass."""
|
|
142
|
+
|
|
143
|
+
pass_name: str
|
|
144
|
+
bytes_deleted: int = 0
|
|
145
|
+
run_count: int = 0
|
|
146
|
+
test_evaluations: int = 0
|
|
147
|
+
successful_reductions: int = 0
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def success_rate(self) -> float:
|
|
151
|
+
"""Percentage of test evaluations that led to a reduction."""
|
|
152
|
+
if self.test_evaluations == 0:
|
|
153
|
+
return 0.0
|
|
154
|
+
return (self.successful_reductions / self.test_evaluations) * 100.0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@define
|
|
158
|
+
class PassStatsTracker:
|
|
159
|
+
"""Tracks statistics for all reduction passes.
|
|
160
|
+
|
|
161
|
+
Python 3.7+ dicts maintain insertion order, so stats are returned
|
|
162
|
+
in the order passes were first run.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
_stats: dict[str, PassStats] = attrs.Factory(dict)
|
|
166
|
+
|
|
167
|
+
def get_or_create(self, pass_name: str) -> PassStats:
|
|
168
|
+
if pass_name not in self._stats:
|
|
169
|
+
self._stats[pass_name] = PassStats(pass_name=pass_name)
|
|
170
|
+
return self._stats[pass_name]
|
|
171
|
+
|
|
172
|
+
def get_stats_in_order(self) -> list[PassStats]:
|
|
173
|
+
"""Get stats in the order passes were first run."""
|
|
174
|
+
return list(self._stats.values())
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class SkipPass(Exception):
|
|
178
|
+
"""Raised to skip the current pass."""
|
|
179
|
+
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
|
|
105
183
|
@define
|
|
106
184
|
class ShrinkRay(Reducer[bytes]):
|
|
107
|
-
clang_delta:
|
|
185
|
+
clang_delta: ClangDelta | None = None
|
|
108
186
|
|
|
109
|
-
|
|
110
|
-
current_pump: Optional[ReductionPump[bytes]] = None
|
|
187
|
+
current_pump: ReductionPump[bytes] | None = None
|
|
111
188
|
|
|
112
189
|
unlocked_ok_passes: bool = False
|
|
190
|
+
pass_stats: PassStatsTracker | None = attrs.Factory(PassStatsTracker)
|
|
191
|
+
|
|
192
|
+
# Pass control: disabled passes and skip functionality
|
|
193
|
+
disabled_passes: set[str] = attrs.Factory(set)
|
|
194
|
+
_skip_requested: bool = attrs.field(default=False, init=False)
|
|
195
|
+
_current_pass_scope: "trio.CancelScope | None" = attrs.field(default=None, init=False)
|
|
196
|
+
_passes_were_skipped: bool = attrs.field(default=False, init=False)
|
|
197
|
+
|
|
198
|
+
def disable_pass(self, pass_name: str) -> None:
|
|
199
|
+
"""Disable a pass by name. If it's currently running, skip it."""
|
|
200
|
+
self.disabled_passes.add(pass_name)
|
|
201
|
+
# If this pass is currently running, skip it
|
|
202
|
+
if (
|
|
203
|
+
self.current_reduction_pass is not None
|
|
204
|
+
and self.current_reduction_pass.__name__ == pass_name
|
|
205
|
+
):
|
|
206
|
+
self.skip_current_pass()
|
|
207
|
+
|
|
208
|
+
def enable_pass(self, pass_name: str) -> None:
|
|
209
|
+
"""Enable a previously disabled pass."""
|
|
210
|
+
self.disabled_passes.discard(pass_name)
|
|
211
|
+
|
|
212
|
+
def is_pass_disabled(self, pass_name: str) -> bool:
|
|
213
|
+
"""Check if a pass is disabled."""
|
|
214
|
+
return pass_name in self.disabled_passes
|
|
215
|
+
|
|
216
|
+
def skip_current_pass(self) -> None:
|
|
217
|
+
"""Request to skip the currently running pass."""
|
|
218
|
+
self._skip_requested = True
|
|
219
|
+
if self._current_pass_scope is not None:
|
|
220
|
+
self._current_pass_scope.cancel()
|
|
113
221
|
|
|
114
222
|
initial_cuts: list[ReductionPass[bytes]] = attrs.Factory(
|
|
115
223
|
lambda: [
|
|
@@ -131,13 +239,13 @@ class ShrinkRay(Reducer[bytes]):
|
|
|
131
239
|
remove_indents,
|
|
132
240
|
hollow,
|
|
133
241
|
lift_braces,
|
|
134
|
-
delete_byte_spans,
|
|
135
242
|
debracket,
|
|
136
243
|
]
|
|
137
244
|
)
|
|
138
245
|
|
|
139
246
|
ok_passes: list[ReductionPass[bytes]] = attrs.Factory(
|
|
140
247
|
lambda: [
|
|
248
|
+
delete_byte_spans,
|
|
141
249
|
compose(Split(b"\n"), block_deletion(11, 20)),
|
|
142
250
|
remove_indents,
|
|
143
251
|
remove_whitespace,
|
|
@@ -149,6 +257,7 @@ class ShrinkRay(Reducer[bytes]):
|
|
|
149
257
|
lexeme_based_deletions,
|
|
150
258
|
short_deletions,
|
|
151
259
|
normalize_identifiers,
|
|
260
|
+
line_sorter,
|
|
152
261
|
]
|
|
153
262
|
)
|
|
154
263
|
|
|
@@ -169,21 +278,21 @@ class ShrinkRay(Reducer[bytes]):
|
|
|
169
278
|
|
|
170
279
|
def __attrs_post_init__(self) -> None:
|
|
171
280
|
if is_python(self.target.current_test_case):
|
|
172
|
-
self.great_passes
|
|
173
|
-
self.initial_cuts
|
|
281
|
+
self.great_passes.extend(PYTHON_PASSES)
|
|
282
|
+
self.initial_cuts.extend(PYTHON_PASSES)
|
|
174
283
|
self.register_format_specific_pass(JSON, JSON_PASSES)
|
|
175
284
|
self.register_format_specific_pass(
|
|
176
285
|
DimacsCNF,
|
|
177
286
|
SAT_PASSES,
|
|
178
287
|
)
|
|
179
288
|
|
|
180
|
-
def register_format_specific_pass(
|
|
289
|
+
def register_format_specific_pass[T](
|
|
181
290
|
self, format: Format[bytes, T], passes: Iterable[ReductionPass[T]]
|
|
182
291
|
):
|
|
183
292
|
if format.is_valid(self.target.current_test_case):
|
|
184
293
|
composed = [compose(format, p) for p in passes]
|
|
185
|
-
self.great_passes
|
|
186
|
-
self.initial_cuts
|
|
294
|
+
self.great_passes.extend(composed)
|
|
295
|
+
self.initial_cuts.extend(composed)
|
|
187
296
|
|
|
188
297
|
@property
|
|
189
298
|
def pumps(self) -> Iterable[ReductionPump[bytes]]:
|
|
@@ -206,12 +315,39 @@ class ShrinkRay(Reducer[bytes]):
|
|
|
206
315
|
return f"Running reduction pump {self.current_pump.__name__}"
|
|
207
316
|
|
|
208
317
|
async def run_pass(self, rp: ReductionPass[bytes]) -> None:
|
|
318
|
+
pass_name = rp.__name__
|
|
319
|
+
|
|
320
|
+
# Skip if pass is disabled
|
|
321
|
+
if self.is_pass_disabled(pass_name):
|
|
322
|
+
return
|
|
323
|
+
|
|
209
324
|
try:
|
|
210
325
|
assert self.current_reduction_pass is None
|
|
211
326
|
self.current_reduction_pass = rp
|
|
212
|
-
|
|
327
|
+
self._skip_requested = False
|
|
328
|
+
|
|
329
|
+
# Get or create stats entry for this pass
|
|
330
|
+
assert self.pass_stats is not None # Always set by Factory
|
|
331
|
+
stats_entry = self.pass_stats.get_or_create(pass_name)
|
|
332
|
+
stats_entry.run_count += 1
|
|
333
|
+
|
|
334
|
+
# Set current pass stats on the problem for real-time updates
|
|
335
|
+
self.target.current_pass_stats = stats_entry
|
|
336
|
+
|
|
337
|
+
# Run the pass with a cancel scope that can be externally cancelled
|
|
338
|
+
with trio.CancelScope() as scope:
|
|
339
|
+
self._current_pass_scope = scope
|
|
340
|
+
await rp(self.target)
|
|
341
|
+
|
|
342
|
+
# If the pass was cancelled/skipped, mark that passes were skipped
|
|
343
|
+
if scope.cancelled_caught:
|
|
344
|
+
self._passes_were_skipped = True
|
|
345
|
+
|
|
213
346
|
finally:
|
|
214
347
|
self.current_reduction_pass = None
|
|
348
|
+
self.target.current_pass_stats = None
|
|
349
|
+
self._current_pass_scope = None
|
|
350
|
+
self._skip_requested = False
|
|
215
351
|
|
|
216
352
|
async def pump(self, rp: ReductionPump[bytes]) -> None:
|
|
217
353
|
try:
|
|
@@ -237,8 +373,24 @@ class ShrinkRay(Reducer[bytes]):
|
|
|
237
373
|
self.current_pump = None
|
|
238
374
|
|
|
239
375
|
async def run_great_passes(self) -> None:
|
|
240
|
-
|
|
241
|
-
|
|
376
|
+
current = self.great_passes
|
|
377
|
+
while True:
|
|
378
|
+
prev = self.target.current_test_case
|
|
379
|
+
successful = []
|
|
380
|
+
for rp in current:
|
|
381
|
+
size = self.target.current_size
|
|
382
|
+
await self.run_pass(rp)
|
|
383
|
+
if self.target.current_size < size:
|
|
384
|
+
successful.append(rp)
|
|
385
|
+
if self.target.current_test_case == prev:
|
|
386
|
+
if len(current) == len(self.great_passes):
|
|
387
|
+
break
|
|
388
|
+
else:
|
|
389
|
+
current = self.great_passes
|
|
390
|
+
elif not successful:
|
|
391
|
+
current = self.great_passes
|
|
392
|
+
else:
|
|
393
|
+
current = successful
|
|
242
394
|
|
|
243
395
|
async def run_ok_passes(self) -> None:
|
|
244
396
|
for rp in self.ok_passes:
|
|
@@ -325,6 +477,9 @@ class ShrinkRay(Reducer[bytes]):
|
|
|
325
477
|
await self.initial_cut()
|
|
326
478
|
|
|
327
479
|
while True:
|
|
480
|
+
# Reset skip tracking for this iteration
|
|
481
|
+
self._passes_were_skipped = False
|
|
482
|
+
|
|
328
483
|
prev = self.target.current_test_case
|
|
329
484
|
await self.run_some_passes()
|
|
330
485
|
if self.target.current_test_case != prev:
|
|
@@ -332,7 +487,10 @@ class ShrinkRay(Reducer[bytes]):
|
|
|
332
487
|
for pump in self.pumps:
|
|
333
488
|
await self.pump(pump)
|
|
334
489
|
if self.target.current_test_case == prev:
|
|
335
|
-
|
|
490
|
+
# Only terminate if no passes were skipped
|
|
491
|
+
# If passes were skipped, we need another full run to be sure
|
|
492
|
+
if not self._passes_were_skipped:
|
|
493
|
+
break
|
|
336
494
|
|
|
337
495
|
|
|
338
496
|
class UpdateKeys(Patches[dict[str, bytes], dict[str, bytes]]):
|
|
@@ -374,6 +532,10 @@ class KeyProblem(ReductionProblem[bytes]):
|
|
|
374
532
|
def current_test_case(self) -> bytes:
|
|
375
533
|
return self.base_problem.current_test_case[self.key]
|
|
376
534
|
|
|
535
|
+
@property
|
|
536
|
+
def stats(self) -> ReductionStats:
|
|
537
|
+
return self.base_problem.stats
|
|
538
|
+
|
|
377
539
|
async def is_interesting(self, test_case: bytes) -> bool:
|
|
378
540
|
return await self.applier.try_apply_patch({self.key: test_case})
|
|
379
541
|
|
|
@@ -389,7 +551,7 @@ class KeyProblem(ReductionProblem[bytes]):
|
|
|
389
551
|
|
|
390
552
|
@define
|
|
391
553
|
class DirectoryShrinkRay(Reducer[dict[str, bytes]]):
|
|
392
|
-
clang_delta:
|
|
554
|
+
clang_delta: ClangDelta | None = None
|
|
393
555
|
|
|
394
556
|
async def run(self):
|
|
395
557
|
prev = None
|