Skip to content

Commit bde46ab

Browse files
pyk.ktool.kfuzz: Add support for custom substitution functions in fuzz (#4792)
Adds an optional `subst_func` parameter to the `fuzz` function, allowing users to customize how substitutions are applied to the template. By default, substitutions are applied using a bottom-up traversal on the whole template pattern. Custom substitution functions can be used to target specific parts of the configuration, enabling more optimized fuzzing. Also includes a test case demonstrating the use of a custom substitution function. --------- Co-authored-by: Tamás Tóth <[email protected]>
1 parent f30f0e3 commit bde46ab

File tree

3 files changed

+120
-14
lines changed

3 files changed

+120
-14
lines changed

pyk/src/pyk/kore/manip.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from .syntax import And, App, EVar, MLQuant, Top
66

77
if TYPE_CHECKING:
8-
from collections.abc import Collection
8+
from collections.abc import Collection, Mapping
99

1010
from .syntax import Pattern
1111

@@ -57,6 +57,24 @@ def add_symbol(pattern: Pattern) -> None:
5757
return res
5858

5959

60+
def substitute_vars(pattern: Pattern, subst_map: Mapping[EVar, Pattern]) -> Pattern:
61+
"""Substitute variables in a pattern using a bottom-up traversal.
62+
63+
Args:
64+
pattern: The pattern containing variables to be substituted.
65+
subst_map: A mapping from variables to their replacement patterns.
66+
"""
67+
68+
def subst(pattern: Pattern) -> Pattern:
69+
match pattern:
70+
case EVar() as var:
71+
return subst_map.get(var, var)
72+
case _:
73+
return pattern
74+
75+
return pattern.bottom_up(subst)
76+
77+
6078
def elim_aliases(pattern: Pattern) -> Pattern:
6179
r"""Eliminate subpatterns of the form ``\and{S}(p, X : S)``.
6280
@@ -66,19 +84,12 @@ def elim_aliases(pattern: Pattern) -> Pattern:
6684

6785
def inline_aliases(pattern: Pattern) -> Pattern:
6886
match pattern:
69-
case And(_, (p, EVar(name))):
70-
aliases[name] = p
87+
case And(_, (p, EVar() as var)):
88+
aliases[var] = p
7189
return p
7290
case _:
7391
return pattern
7492

75-
def substitute_vars(pattern: Pattern) -> Pattern:
76-
match pattern:
77-
case EVar(name) as var:
78-
return aliases.get(name, var)
79-
case _:
80-
return pattern
81-
8293
pattern = pattern.bottom_up(inline_aliases)
83-
pattern = pattern.bottom_up(substitute_vars)
94+
pattern = substitute_vars(pattern, aliases)
8495
return pattern

pyk/src/pyk/ktool/kfuzz.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from hypothesis import Phase, given, settings
77
from hypothesis.strategies import fixed_dictionaries, integers
88

9+
from ..kore.manip import substitute_vars
910
from ..kore.parser import KoreParser
1011
from ..kore.prelude import inj
1112
from ..kore.syntax import DV, EVar, SortApp, String
@@ -54,10 +55,17 @@ class KFuzz:
5455

5556
definition_dir: Path
5657
handler: KFuzzHandler
58+
subst_func: Callable[[Pattern, Mapping[EVar, Pattern]], Pattern]
5759

58-
def __init__(self, definition_dir: Path, handler: KFuzzHandler = _DEFAULT_HANDLER) -> None:
60+
def __init__(
61+
self,
62+
definition_dir: Path,
63+
handler: KFuzzHandler = _DEFAULT_HANDLER,
64+
subst_func: Callable[[Pattern, Mapping[EVar, Pattern]], Pattern] = substitute_vars,
65+
) -> None:
5966
self.definition_dir = definition_dir
6067
self.handler = handler
68+
self.subst_func = subst_func
6169

6270
def fuzz_with_check(
6371
self,
@@ -76,6 +84,7 @@ def fuzz_with_check(
7684
subst_strategy,
7785
check_func=check_func,
7886
handler=self.handler,
87+
subst_func=self.subst_func,
7988
**hypothesis_args,
8089
)
8190

@@ -95,6 +104,7 @@ def fuzz_with_exit_code(
95104
subst_strategy,
96105
check_exit_code=True,
97106
handler=self.handler,
107+
subst_func=self.subst_func,
98108
**hypothesis_args,
99109
)
100110

@@ -133,6 +143,7 @@ def fuzz(
133143
check_func: Callable[[Pattern], Any] | None = None,
134144
check_exit_code: bool = False,
135145
handler: KFuzzHandler = _DEFAULT_HANDLER,
146+
subst_func: Callable[[Pattern, Mapping[EVar, Pattern]], Pattern] = substitute_vars,
136147
**hypothesis_args: Any,
137148
) -> None:
138149
"""Fuzz a property test with concrete execution over a K term.
@@ -148,6 +159,9 @@ def fuzz(
148159
An exit code of 0 indicates a passing test.
149160
A RuntimeError will be thrown if this is True and check_func is also passed as an argument.
150161
handler: An instance of a `KFuzzHandler` implementing custom behavior while fuzzing.
162+
subst_func: A function used to apply the substitution to the template.
163+
Takes the original template and a substitution map, and returns the substituted pattern.
164+
Defaults to a bottom-up traversal that replaces variables with the corresponding values in the substitution map.
151165
hypothesis_args: Keyword arguments that will be passed as settings for the hypothesis test. Defaults:
152166
153167
deadline: 5000
@@ -169,7 +183,7 @@ def sub(p: Pattern) -> Pattern:
169183
return p
170184

171185
handler.handle_test(subst_case)
172-
test_pattern = template.bottom_up(sub)
186+
test_pattern = subst_func(template, subst_case)
173187
res = llvm_interpret_raw(definition_dir, test_pattern.text, check=False)
174188

175189
try:

pyk/src/tests/integration/ktool/test_fuzz.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55

66
import pytest
77

8+
from pyk.kore.manip import substitute_vars
89
from pyk.kore.parser import KoreParser
9-
from pyk.kore.prelude import inj, top_cell_initializer
10+
from pyk.kore.prelude import generated_counter, generated_top, inj, int_dv, k, map_pattern, top_cell_initializer
1011
from pyk.kore.syntax import DV, App, EVar, SortApp, String
1112
from pyk.ktool.kfuzz import KFuzz, kintegers
1213
from pyk.ktool.kprint import _kast
@@ -15,6 +16,7 @@
1516
from ..utils import K_FILES, TEST_DATA_DIR
1617

1718
if TYPE_CHECKING:
19+
from collections.abc import Mapping
1820
from pathlib import Path
1921
from typing import Final
2022

@@ -28,6 +30,37 @@
2830
VAR_Y = EVar(name='VarY', sort=SortApp('SortInt'))
2931

3032

33+
def t(k_cell: Pattern, state_cell: Pattern) -> App:
34+
return App("Lbl'-LT-'T'-GT-'", (), (k_cell, state_cell))
35+
36+
37+
def state(pattern: Pattern) -> App:
38+
return App("Lbl'-LT-'state'-GT-'", (), (pattern,))
39+
40+
41+
def subst_on_k_cell(template: Pattern, subst_case: Mapping[EVar, Pattern]) -> Pattern:
42+
"""A substitution function that only applies substitutions within the K cell.
43+
Used to test custom substitution option in fuzzing and optimize fuzzers by
44+
restricting changes to relevant parts of the configuration.
45+
46+
Args:
47+
template: The template configuration containing variables in the K cell.
48+
subst_case: A mapping from variables to their replacement patterns.
49+
"""
50+
match template:
51+
case App(
52+
"Lbl'-LT-'generatedTop'-GT-'",
53+
args=(
54+
App("Lbl'-LT-'T'-GT-'", args=(k_cell, state_cell)),
55+
generated_counter_cell,
56+
),
57+
):
58+
k_cell = substitute_vars(k_cell, subst_case)
59+
return generated_top((t(k_cell, state_cell), generated_counter_cell))
60+
61+
raise ValueError(template)
62+
63+
3164
class TestImpFuzz(KompiledTest):
3265
KOMPILE_MAIN_FILE = K_FILES / 'imp.k'
3366
KOMPILE_BACKEND = 'llvm'
@@ -37,6 +70,10 @@ class TestImpFuzz(KompiledTest):
3770
def kfuzz(self, definition_dir: Path) -> KFuzz:
3871
return KFuzz(definition_dir)
3972

73+
@pytest.fixture
74+
def kfuzz_with_subst(self, definition_dir: Path) -> KFuzz:
75+
return KFuzz(definition_dir, subst_func=subst_on_k_cell)
76+
4077
@staticmethod
4178
def check(p: Pattern) -> None:
4279
def check_inner(p: Pattern) -> Pattern:
@@ -76,6 +113,25 @@ def replace_var_ids(p: Pattern) -> Pattern:
76113

77114
return init_pattern
78115

116+
@staticmethod
117+
def setup_program_with_config(definition_dir: Path, text: str) -> Pattern:
118+
119+
kore_text = _kast(definition_dir=definition_dir, input='program', output='kore', expression=text).stdout
120+
121+
program_pattern = KoreParser(kore_text).pattern()
122+
123+
def replace_var_ids(p: Pattern) -> Pattern:
124+
match p:
125+
case App('inj', _, (DV(_, String('varx')),)):
126+
return VAR_X
127+
case App('inj', _, (DV(_, String('vary')),)):
128+
return VAR_Y
129+
return p
130+
131+
program_pattern = program_pattern.top_down(replace_var_ids)
132+
133+
return generated_top((t(k(program_pattern), state(map_pattern())), generated_counter(int_dv(0))))
134+
79135
def test_fuzz(
80136
self,
81137
definition_dir: Path,
@@ -124,3 +180,28 @@ def test_fuzz_fail(
124180
# Then
125181
with pytest.raises(AssertionError):
126182
kfuzz.fuzz_with_check(init_pattern, self.SUBSTS, self.check)
183+
184+
def test_fuzz_with_subst(
185+
self,
186+
definition_dir: Path,
187+
kfuzz_with_subst: KFuzz,
188+
) -> None:
189+
# Given
190+
program_text = """
191+
// Checks the commutativity of addition
192+
int x, y, a, b, res;
193+
x = varx;
194+
y = vary;
195+
a = x + y;
196+
b = y + x;
197+
if ((a <= b) && (b <= a)) { // a == b
198+
res = 0;
199+
} else {
200+
res = 1;
201+
}
202+
"""
203+
204+
init_pattern = self.setup_program_with_config(definition_dir, program_text)
205+
206+
# Then
207+
kfuzz_with_subst.fuzz_with_check(init_pattern, self.SUBSTS, self.check)

0 commit comments

Comments
 (0)