shrinkray 25.12.27.2__tar.gz → 25.12.28.0__tar.gz

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 (69) hide show
  1. {shrinkray-25.12.27.2/src/shrinkray.egg-info → shrinkray-25.12.28.0}/PKG-INFO +1 -1
  2. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/pyproject.toml +1 -1
  3. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/__main__.py +25 -11
  4. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/passes/bytes.py +0 -125
  5. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/passes/clangdelta.py +0 -24
  6. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/passes/genericlanguages.py +0 -20
  7. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/passes/json.py +0 -8
  8. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/passes/patching.py +0 -63
  9. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/passes/sequences.py +0 -25
  10. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/reducer.py +0 -50
  11. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/state.py +192 -56
  12. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/subprocess/__init__.py +0 -4
  13. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/subprocess/client.py +2 -0
  14. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/subprocess/protocol.py +8 -11
  15. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/subprocess/worker.py +67 -25
  16. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/tui.py +114 -92
  17. shrinkray-25.12.28.0/src/shrinkray/validation.py +403 -0
  18. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0/src/shrinkray.egg-info}/PKG-INFO +1 -1
  19. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray.egg-info/SOURCES.txt +2 -2
  20. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_byte_reduction_passes.py +1 -97
  21. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_clang_delta.py +2 -47
  22. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_generic_language.py +0 -31
  23. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_generic_shrinking_properties.py +0 -15
  24. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_json_passes.py +0 -43
  25. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_main.py +40 -35
  26. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_patching.py +0 -140
  27. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_python_reducers.py +0 -11
  28. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_reducer.py +1 -13
  29. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_reduction_passes.py +8 -169
  30. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_state.py +209 -0
  31. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_subprocess_protocol.py +0 -32
  32. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_subprocess_worker.py +99 -14
  33. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_tui.py +158 -155
  34. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_tui_snapshots.py +108 -0
  35. shrinkray-25.12.28.0/tests/test_validation.py +1131 -0
  36. shrinkray-25.12.27.2/src/shrinkray/display.py +0 -75
  37. shrinkray-25.12.27.2/tests/test_display.py +0 -135
  38. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/LICENSE +0 -0
  39. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/README.md +0 -0
  40. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/setup.cfg +0 -0
  41. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/__init__.py +0 -0
  42. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/cli.py +0 -0
  43. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/formatting.py +0 -0
  44. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/passes/__init__.py +0 -0
  45. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/passes/definitions.py +0 -0
  46. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/passes/python.py +0 -0
  47. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/passes/sat.py +0 -0
  48. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/problem.py +0 -0
  49. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/process.py +0 -0
  50. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/py.typed +0 -0
  51. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/ui.py +0 -0
  52. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray/work.py +0 -0
  53. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray.egg-info/dependency_links.txt +0 -0
  54. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray.egg-info/entry_points.txt +0 -0
  55. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray.egg-info/requires.txt +0 -0
  56. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/src/shrinkray.egg-info/top_level.txt +0 -0
  57. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_cli.py +0 -0
  58. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_definitions.py +0 -0
  59. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_dimacs_cnf.py +0 -0
  60. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_formatting.py +0 -0
  61. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_misc_reduction_performance.py +0 -0
  62. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_natural_sort_orders.py +0 -0
  63. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_problem.py +0 -0
  64. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_process.py +0 -0
  65. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_sat.py +0 -0
  66. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_subprocess_client.py +0 -0
  67. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_subprocess_integration.py +0 -0
  68. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_ui.py +0 -0
  69. {shrinkray-25.12.27.2 → shrinkray-25.12.28.0}/tests/test_work.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shrinkray
3
- Version: 25.12.27.2
3
+ Version: 25.12.28.0
4
4
  Summary: Shrink Ray
5
5
  Author-email: "David R. MacIver" <david@drmaciver.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shrinkray"
3
- version = "25.12.27.2"
3
+ version = "25.12.28.0"
4
4
  description = "Shrink Ray"
5
5
  authors = [
6
6
  {name = "David R. MacIver", email = "david@drmaciver.com"}
@@ -17,18 +17,19 @@ from shrinkray.cli import (
17
17
  validate_command,
18
18
  validate_ui,
19
19
  )
20
+ from shrinkray.formatting import determine_formatter_command
20
21
  from shrinkray.passes.clangdelta import (
21
22
  C_FILE_EXTENSIONS,
22
23
  ClangDelta,
23
24
  find_clang_delta,
24
25
  )
25
- from shrinkray.problem import InvalidInitialExample
26
26
  from shrinkray.state import (
27
27
  ShrinkRayDirectoryState,
28
28
  ShrinkRayState,
29
29
  ShrinkRayStateSingleFile,
30
30
  )
31
31
  from shrinkray.ui import BasicUI, ShrinkRayUI
32
+ from shrinkray.validation import run_validation
32
33
  from shrinkray.work import Volume
33
34
 
34
35
 
@@ -39,12 +40,9 @@ async def run_shrink_ray(
39
40
  """Run the shrink ray reduction process."""
40
41
  async with trio.open_nursery() as nursery:
41
42
  problem = state.problem
42
- try:
43
- await problem.setup()
44
- except* InvalidInitialExample as excs:
45
- assert len(excs.exceptions) == 1
46
- (e,) = excs.exceptions
47
- await state.report_error(e)
43
+ # Validation runs before run_shrink_ray is called, so setup() should
44
+ # always succeed. If it doesn't, there's a bug and we want it to propagate.
45
+ await problem.setup()
48
46
 
49
47
  reducer = state.reducer
50
48
 
@@ -273,6 +271,26 @@ def main(
273
271
  if not backup:
274
272
  backup = filename + os.extsep + "bak"
275
273
 
274
+ # Run initial validation before any state setup
275
+ # This validates the interestingness test and formatter with proper output streaming
276
+ formatter_command = None
277
+ if not os.path.isdir(filename) and formatter.lower() != "none":
278
+ formatter_command = determine_formatter_command(formatter, filename)
279
+
280
+ validation_result = run_validation(
281
+ file_path=filename,
282
+ test=test,
283
+ input_type=input_type,
284
+ in_place=in_place,
285
+ formatter_command=formatter_command,
286
+ )
287
+
288
+ if not validation_result.success:
289
+ print(f"\nError: {validation_result.error_message}", file=sys.stderr)
290
+ sys.exit(1)
291
+
292
+ print("\nStarting reduction...", file=sys.stderr, flush=True)
293
+
276
294
  state_kwargs: dict[str, Any] = {
277
295
  "input_type": input_type,
278
296
  "in_place": in_place,
@@ -307,8 +325,6 @@ def main(
307
325
 
308
326
  state = ShrinkRayDirectoryState(initial=initial, **state_kwargs)
309
327
 
310
- trio.run(state.check_formatter)
311
-
312
328
  else:
313
329
  try:
314
330
  os.remove(backup)
@@ -323,8 +339,6 @@ def main(
323
339
 
324
340
  state = ShrinkRayStateSingleFile(initial=initial, **state_kwargs)
325
341
 
326
- trio.run(state.check_formatter)
327
-
328
342
  if ui_type == UIType.textual:
329
343
  from shrinkray.tui import run_textual_ui
330
344
 
@@ -28,26 +28,6 @@ from shrinkray.passes.patching import Cuts, Patches, apply_patches
28
28
  from shrinkray.problem import Format, ReductionProblem
29
29
 
30
30
 
31
- @define(frozen=True)
32
- class Encoding(Format[bytes, str]):
33
- """Format that decodes/encodes bytes using a character encoding."""
34
-
35
- encoding: str
36
-
37
- def __repr__(self) -> str:
38
- return f"Encoding({repr(self.encoding)})"
39
-
40
- @property
41
- def name(self) -> str:
42
- return self.encoding
43
-
44
- def parse(self, input: bytes) -> str:
45
- return input.decode(self.encoding)
46
-
47
- def dumps(self, input: str) -> bytes:
48
- return input.encode(self.encoding)
49
-
50
-
51
31
  @define(frozen=True)
52
32
  class Split(Format[bytes, list[bytes]]):
53
33
  """Format that splits bytes by a delimiter.
@@ -165,9 +145,6 @@ def tokenize(text: bytes) -> list[bytes]:
165
145
  return result
166
146
 
167
147
 
168
- MAX_DELETE_INTERVAL = 8
169
-
170
-
171
148
  async def lexeme_based_deletions(
172
149
  problem: ReductionProblem[bytes], min_size: int = 8
173
150
  ) -> None:
@@ -601,108 +578,6 @@ async def lower_individual_bytes(problem: ReductionProblem[bytes]) -> None:
601
578
  await apply_patches(problem, IndividualByteReplacement(), patches)
602
579
 
603
580
 
604
- RegionReplacementPatch = list[tuple[int, int, int]]
605
-
606
-
607
- class RegionReplacement(Patches[RegionReplacementPatch, bytes]):
608
- @property
609
- def empty(self) -> RegionReplacementPatch:
610
- return []
611
-
612
- def combine(self, *patches: RegionReplacementPatch) -> RegionReplacementPatch:
613
- result: RegionReplacementPatch = []
614
- for p in patches:
615
- result.extend(p)
616
- return result
617
-
618
- def apply(self, patch: RegionReplacementPatch, target: bytes) -> bytes:
619
- result = bytearray(target)
620
- for i, j, d in patch:
621
- if d < result[i]:
622
- for k in range(i, j):
623
- result[k] = d
624
- return bytes(result)
625
-
626
- def size(self, patch: RegionReplacementPatch) -> int:
627
- return 0
628
-
629
-
630
- async def short_replacements(problem: ReductionProblem[bytes]) -> None:
631
- """Replace short regions with uniform byte values.
632
-
633
- Tries replacing 1-4 byte regions with uniform values like 0, 1,
634
- space, newline, or period. Useful for simplifying small sequences.
635
- """
636
- target = problem.current_test_case
637
- patches = [
638
- [(i, j, c)]
639
- for c in [0, 1] + list(b"01 \t\n\r.")
640
- for i in range(len(target))
641
- if target[i] > c
642
- for j in range(i + 1, min(i + 5, len(target) + 1))
643
- ]
644
-
645
- await apply_patches(problem, RegionReplacement(), patches)
646
-
647
-
648
- WHITESPACE = b" \t\r\n"
649
-
650
-
651
- async def sort_whitespace(problem: ReductionProblem[bytes]) -> None:
652
- """NB: This is a stupid pass that we only really need for artificial
653
- test cases, but it's helpful for allowing those artificial test cases
654
- to expose other issues."""
655
-
656
- whitespace_up_to = 0
657
- while (
658
- whitespace_up_to < len(problem.current_test_case)
659
- and problem.current_test_case[whitespace_up_to] not in WHITESPACE
660
- ):
661
- whitespace_up_to += 1
662
- while (
663
- whitespace_up_to < len(problem.current_test_case)
664
- and problem.current_test_case[whitespace_up_to] in WHITESPACE
665
- ):
666
- whitespace_up_to += 1
667
-
668
- # If the initial whitespace ends with a newline we want to keep it doing
669
- # that. This is mostly for Python purposes.
670
- if (
671
- whitespace_up_to > 0
672
- and problem.current_test_case[whitespace_up_to - 1] == b"\n"[0]
673
- ):
674
- whitespace_up_to -= 1
675
-
676
- i = whitespace_up_to + 1
677
-
678
- while i < len(problem.current_test_case):
679
- if problem.current_test_case[i] not in WHITESPACE:
680
- i += 1
681
- continue
682
-
683
- async def can_move_to_whitespace(k: int) -> bool:
684
- if i + k > len(problem.current_test_case):
685
- return False
686
-
687
- base = problem.current_test_case
688
- target = base[i : i + k]
689
-
690
- if any(c not in WHITESPACE for c in target):
691
- return False
692
-
693
- prefix = base[:whitespace_up_to]
694
- attempt = prefix + target + base[whitespace_up_to:i] + base[i + k :]
695
- return await problem.is_interesting(attempt)
696
-
697
- k = await problem.work.find_large_integer(can_move_to_whitespace)
698
- whitespace_up_to += k
699
- i += k + 1
700
- test_case = problem.current_test_case
701
- await problem.is_interesting(
702
- bytes(sorted(test_case[:whitespace_up_to])) + test_case[whitespace_up_to:]
703
- )
704
-
705
-
706
581
  # These are some cheat substitutions that are sometimes helpful, but mostly
707
582
  # for passing stupid tests.
708
583
  STANDARD_SUBSTITUTIONS = [(b"\0\0", b"\1"), (b"\0\0", b"\xff")]
@@ -1,6 +1,5 @@
1
1
  import os
2
2
  import subprocess
3
- from functools import lru_cache
4
3
  from glob import glob
5
4
  from shutil import which
6
5
  from tempfile import NamedTemporaryFile
@@ -26,29 +25,6 @@ def find_clang_delta():
26
25
  return clang_delta
27
26
 
28
27
 
29
- @lru_cache(maxsize=1)
30
- def clang_delta_works() -> bool:
31
- """Check if clang_delta can actually execute.
32
-
33
- This verifies not just that the binary exists, but that it can run.
34
- On some systems (e.g., Ubuntu 24.04), creduce is installed but
35
- clang_delta fails at runtime due to shared library issues.
36
- """
37
- clang_delta = find_clang_delta()
38
- if not clang_delta:
39
- return False
40
- try:
41
- # Run a simple test to verify clang_delta works
42
- result = subprocess.run(
43
- [clang_delta, "--help"],
44
- capture_output=True,
45
- timeout=5,
46
- )
47
- return result.returncode == 0
48
- except (OSError, subprocess.TimeoutExpired):
49
- return False
50
-
51
-
52
28
  TRANSFORMATIONS: list[str] = [
53
29
  "aggregate-to-scalar",
54
30
  "binop-simplification",
@@ -9,7 +9,6 @@ from string import ascii_lowercase, ascii_uppercase
9
9
  from typing import AnyStr
10
10
 
11
11
  import trio
12
- from attr import define
13
12
 
14
13
  from shrinkray.passes.bytes import ByteReplacement, delete_intervals
15
14
  from shrinkray.passes.definitions import ReductionPass
@@ -24,25 +23,6 @@ from shrinkray.problem import (
24
23
  from shrinkray.work import NotFound
25
24
 
26
25
 
27
- @define(frozen=True)
28
- class Substring(Format[AnyStr, AnyStr]):
29
- prefix: AnyStr
30
- suffix: AnyStr
31
-
32
- @property
33
- def name(self) -> str:
34
- return f"Substring({len(self.prefix)}, {len(self.suffix)})"
35
-
36
- def parse(self, input: AnyStr) -> AnyStr:
37
- if input.startswith(self.prefix) and input.endswith(self.suffix):
38
- return input[len(self.prefix) : len(input) - len(self.suffix)]
39
- else:
40
- raise ParseError()
41
-
42
- def dumps(self, input: AnyStr) -> AnyStr:
43
- return self.prefix + input + self.suffix
44
-
45
-
46
26
  class RegionReplacingPatches(Patches[dict[int, AnyStr], AnyStr]):
47
27
  def __init__(self, regions: list[tuple[int, int]]):
48
28
  assert regions
@@ -9,14 +9,6 @@ from shrinkray.passes.patching import Patches, apply_patches
9
9
  from shrinkray.problem import Format, ParseError, ReductionProblem
10
10
 
11
11
 
12
- def is_json(s: bytes) -> bool:
13
- try:
14
- json.loads(s)
15
- return True
16
- except ValueError:
17
- return False
18
-
19
-
20
12
  @define(frozen=True)
21
13
  class _JSON(Format[bytes, Any]):
22
14
  def __repr__(self) -> str:
@@ -1,7 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from collections.abc import Callable, Iterable, Sequence
3
- from enum import Enum
4
- from random import Random
5
3
  from typing import Any, TypeVar, cast
6
4
 
7
5
  import trio
@@ -52,27 +50,6 @@ class SetPatches[T, TargetType](Patches[frozenset[T], TargetType]):
52
50
  return len(patch)
53
51
 
54
52
 
55
- class ListPatches[T, TargetType](Patches[list[T], TargetType]):
56
- def __init__(self, apply: Callable[[list[T], TargetType], TargetType]):
57
- self.__apply = apply
58
-
59
- @property
60
- def empty(self):
61
- return []
62
-
63
- def combine(self, *patches: list[T]) -> list[T]:
64
- result = []
65
- for p in patches:
66
- result.extend(p)
67
- return result
68
-
69
- def apply(self, patch: list[T], target: TargetType) -> TargetType:
70
- return self.__apply(patch, target)
71
-
72
- def size(self, patch: list[T]) -> int:
73
- return len(patch)
74
-
75
-
76
53
  class PatchApplier[PatchType, TargetType]:
77
54
  def __init__(
78
55
  self,
@@ -174,15 +151,6 @@ class PatchApplier[PatchType, TargetType]:
174
151
  return await receive_merge_result.receive()
175
152
 
176
153
 
177
- class Direction(Enum):
178
- LEFT = 0
179
- RIGHT = 1
180
-
181
-
182
- class Completed(Exception):
183
- pass
184
-
185
-
186
154
  async def apply_patches[PatchType, TargetType](
187
155
  problem: ReductionProblem[TargetType],
188
156
  patch_info: Patches[PatchType, TargetType],
@@ -220,37 +188,6 @@ async def apply_patches[PatchType, TargetType](
220
188
  await applier.try_apply_patch(patch)
221
189
 
222
190
 
223
- class LazyMutableRange:
224
- def __init__(self, n: int):
225
- self.__size = n
226
- self.__mask: dict[int, int] = {}
227
-
228
- def __getitem__(self, i: int) -> int:
229
- return self.__mask.get(i, i)
230
-
231
- def __setitem__(self, i: int, v: int) -> None:
232
- self.__mask[i] = v
233
-
234
- def __len__(self) -> int:
235
- return self.__size
236
-
237
- def pop(self) -> int:
238
- i = len(self) - 1
239
- result = self[i]
240
- self.__size = i
241
- self.__mask.pop(i, None)
242
- return result
243
-
244
-
245
- def lazy_shuffle[T](seq: Sequence[T], rnd: Random) -> Iterable[T]:
246
- indices = LazyMutableRange(len(seq))
247
- while indices:
248
- j = len(indices) - 1
249
- i = rnd.randrange(0, len(indices))
250
- indices[i], indices[j] = indices[j], indices[i]
251
- yield seq[indices.pop()]
252
-
253
-
254
191
  CutPatch = list[tuple[int, int]]
255
192
 
256
193
 
@@ -18,31 +18,6 @@ async def delete_elements[Seq: Sequence[Any]](problem: ReductionProblem[Seq]) ->
18
18
  )
19
19
 
20
20
 
21
- def merged_intervals(intervals: list[tuple[int, int]]) -> list[tuple[int, int]]:
22
- normalized: list[list[int]] = []
23
- for start, end in sorted(map(tuple, intervals)):
24
- if normalized and normalized[-1][-1] >= start:
25
- normalized[-1][-1] = max(normalized[-1][-1], end)
26
- else:
27
- normalized.append([start, end])
28
- return list(map(tuple, normalized)) # type: ignore
29
-
30
-
31
- def with_deletions[Seq: Sequence[Any]](
32
- target: Seq, deletions: list[tuple[int, int]]
33
- ) -> Seq:
34
- result: list[Any] = []
35
- prev = 0
36
- total_deleted = 0
37
- for start, end in deletions:
38
- total_deleted += end - start
39
- result.extend(target[prev:start])
40
- prev = end
41
- result.extend(target[prev:])
42
- assert len(result) + total_deleted == len(target)
43
- return type(target)(result) # type: ignore
44
-
45
-
46
21
  def block_deletion[Seq: Sequence[Any]](
47
22
  min_block: int, max_block: int
48
23
  ) -> ReductionPass[Seq]:
@@ -99,50 +99,6 @@ class Reducer[T](ABC):
99
99
  """Skip the currently running pass. Override in subclasses for pass control."""
100
100
 
101
101
 
102
- @define
103
- class BasicReducer[T](Reducer[T]):
104
- reduction_passes: Iterable[ReductionPass[T]]
105
- pumps: Iterable[ReductionPump[T]] = ()
106
- _status: str = "Starting up"
107
-
108
- def __attrs_post_init__(self) -> None:
109
- self.reduction_passes = list(self.reduction_passes)
110
-
111
- @property
112
- def status(self) -> str:
113
- return self._status
114
-
115
- @status.setter
116
- def status(self, value: str) -> None:
117
- self._status = value
118
-
119
- async def run_pass(self, rp: ReductionPass[T]) -> None:
120
- await rp(self.target)
121
-
122
- async def run(self) -> None:
123
- await self.target.setup()
124
-
125
- while True:
126
- prev = self.target.current_test_case
127
- for rp in self.reduction_passes:
128
- self.status = f"Running reduction pass {rp.__name__}"
129
- await self.run_pass(rp)
130
- for pump in self.pumps:
131
- self.status = f"Pumping with {pump.__name__}"
132
- pumped = await pump(self.target)
133
- if pumped != self.target.current_test_case:
134
- with self.backtrack(pumped):
135
- for rp in self.reduction_passes:
136
- self.status = f"Running reduction pass {rp.__name__} under pump {pump.__name__}"
137
- await self.run_pass(rp)
138
- if prev == self.target.current_test_case:
139
- return
140
-
141
-
142
- class RestartPass(Exception):
143
- pass
144
-
145
-
146
102
  @define
147
103
  class PassStats:
148
104
  """Statistics for a single reduction pass."""
@@ -181,12 +137,6 @@ class PassStatsTracker:
181
137
  return list(self._stats.values())
182
138
 
183
139
 
184
- class SkipPass(Exception):
185
- """Raised to skip the current pass."""
186
-
187
- pass
188
-
189
-
190
140
  @define
191
141
  class ShrinkRay(Reducer[bytes]):
192
142
  clang_delta: ClangDelta | None = None