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/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, Generic, Iterable, Optional, TypeVar
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 C_FILE_EXTENSIONS, ClangDelta, clang_delta_pumps
27
- from shrinkray.passes.definitions import Format, ReductionPass, ReductionPump, compose
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(Generic[T], ABC):
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
- status: str = "Starting up"
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: Optional[ClangDelta] = None
185
+ clang_delta: ClangDelta | None = None
108
186
 
109
- current_reduction_pass: Optional[ReductionPass[bytes]] = None
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[:0] = PYTHON_PASSES
173
- self.initial_cuts[:0] = PYTHON_PASSES
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[:0] = composed
186
- self.initial_cuts[:0] = composed
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
- await rp(self.target)
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
- for rp in self.great_passes:
241
- await self.run_pass(rp)
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
- break
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: Optional[ClangDelta] = None
554
+ clang_delta: ClangDelta | None = None
393
555
 
394
556
  async def run(self):
395
557
  prev = None