Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Begin implementing fuzzing #4454

Merged
merged 18 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion pyk/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ cmd2 = "^2.4.2"
coloredlogs = "^15.0.1"
filelock = "^3.9.0"
graphviz = "^0.20.1"
hypothesis = "^6.103.1"
psutil = "5.9.5"
pybind11 = "^2.10.3"
pytest = "*"
Expand Down
111 changes: 111 additions & 0 deletions pyk/src/pyk/ktool/kfuzz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from hypothesis import Phase, Verbosity, given, settings
from hypothesis.strategies import builds, fixed_dictionaries, integers

from ..kast.inner import KSort
from ..konvert import _kast_to_kore
from ..kore.parser import KoreParser
from ..kore.syntax import Assoc, EVar
from ..prelude.k import inj
from ..prelude.kint import intToken
from .krun import llvm_interpret_raw

if TYPE_CHECKING:
from collections.abc import Callable, Mapping
from pathlib import Path
from typing import Any

from hypothesis.strategies import SearchStrategy

from ..kast.inner import KInner
from ..kore.syntax import Pattern


def kintegers(
gtrepta marked this conversation as resolved.
Show resolved Hide resolved
*,
min_value: int | None = None,
max_value: int | None = None,
with_inj: KSort | None = None,
) -> SearchStrategy[Pattern]:
"""Return a search strategy for K integers.

Args:
min_value: Minimum value for the generated integers
max_value: Maximum value for the generated integers
with_inj: Return the integer as an injection into this sort

Returns:
A strategy which generates integer domain values.
"""

def int_dv(value: int) -> Pattern:
res: KInner = intToken(value)
if with_inj is not None:
res = inj(KSort('Int'), with_inj, res)
return _kast_to_kore(res)

return builds(int_dv, integers(min_value=min_value, max_value=max_value))


def fuzz(
definition_dir: str | Path,
template: Pattern,
subst_strategy: dict[EVar, SearchStrategy[Pattern]],
check_func: Callable[[Pattern], Any] | None = None,
check_exit_code: bool = False,
max_examples: int = 50,
) -> None:
"""Fuzz a property test with concrete execution over a K term.

Args:
definition_dir: The location of the K definition to run the interpreter for.
template: The term which will be sent to the interpreter after randomizing inputs. It should contain at least one variable which will be substituted for a value.
subst_strategy: Should have each variable in the template term mapped to a strategy for generating values for it.
check_func: Will be called on the kore output from the interpreter.
Should throw an AssertionError if it determines that the output indicates a test failure.
A RuntimeError will be thrown if this is passed as an argument and check_exit_code is True.
check_exit_code: Check the exit code of the interpreter for a test failure instead of using check_func.
An exit code of 0 indicates a passing test.
A RuntimeError will be thrown if this is True and check_func is also passed as an argument.
max_examples: The number of test cases to run.

Raises:
RuntimeError: If check_func exists and check_exit_code is set, or check_func doesn't exist and check_exit_code is cleared.
"""
if bool(check_func) == check_exit_code:
raise RuntimeError('Must pass one of check_func or check_exit_code, and not both!')

def test(subst_case: Mapping[EVar, Pattern]) -> None:
def sub(p: Pattern) -> Pattern:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider extracting this to kore.manip.

if isinstance(p, Assoc):
symbol = p.symbol()
args = (arg.top_down(sub) for arg in p.app.args)
return p.of(symbol, patterns=(p.app.let(args=args),))
if p in subst_case:
assert isinstance(p, EVar)
return subst_case[p]
return p

test_pattern = template.top_down(sub)
res = llvm_interpret_raw(definition_dir, test_pattern.text)

if check_exit_code:
assert res.returncode == 0
else:
assert check_func
res_pattern = KoreParser(res.stdout).pattern()
check_func(res_pattern)

strat: SearchStrategy = fixed_dictionaries(subst_strategy)

given(strat)(
settings(
deadline=50000,
max_examples=max_examples,
verbosity=Verbosity.verbose,
phases=(Phase.generate, Phase.target, Phase.shrink),
)(test)
)()
31 changes: 23 additions & 8 deletions pyk/src/pyk/ktool/krun.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,18 +315,33 @@ def llvm_interpret(definition_dir: str | Path, pattern: Pattern, *, depth: int |
Raises:
RuntimeError: If the interpreter fails.
"""
definition_dir = Path(definition_dir)
check_dir_path(definition_dir)
try:
res = llvm_interpret_raw(definition_dir, pattern.text, depth)
except CalledProcessError as err:
raise RuntimeError(f'Interpreter failed with status {err.returncode}: {err.stderr}') from err

return KoreParser(res.stdout).pattern()


def llvm_interpret_raw(definition_dir: str | Path, kore: str, depth: int | None = None) -> CompletedProcess:
"""Execute the `interpreter` binary generated by the LLVM Backend, with no processing of input/output.

Args:
definition_dir: Path to the kompiled definition directory.
pattern: KORE string to start rewriting from.
depth: Maximal number of rewrite steps to take.

Returns:
The CompletedProcess of the interpreter.

Raises:
CalledProcessError: If the interpreter fails.
"""
definition_dir = Path(definition_dir)
interpreter_file = definition_dir / 'interpreter'
check_file_path(interpreter_file)

depth = depth if depth is not None else -1
args = [str(interpreter_file), '/dev/stdin', str(depth), '/dev/stdout']

try:
res = run_process(args, input=pattern.text, pipe_stderr=True)
except CalledProcessError as err:
raise RuntimeError(f'Interpreter failed with status {err.returncode}: {err.stderr}') from err

return KoreParser(res.stdout).pattern()
return run_process(args, input=kore, pipe_stderr=True)
125 changes: 125 additions & 0 deletions pyk/src/tests/integration/ktool/test_fuzz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING

import pytest

from pyk.kast.inner import KSort
from pyk.kore.parser import KoreParser
from pyk.kore.prelude import inj, top_cell_initializer
from pyk.kore.syntax import DV, App, Assoc, EVar, SortApp, String
from pyk.ktool.kfuzz import fuzz, kintegers
from pyk.ktool.kprint import _kast
from pyk.testing import KompiledTest

from ..utils import K_FILES, TEST_DATA_DIR

if TYPE_CHECKING:
from pathlib import Path
from typing import Final

from pyk.kore.syntax import Pattern

_LOGGER: Final = logging.getLogger(__name__)

FUZZ_FILES: Path = TEST_DATA_DIR / 'fuzzing'

VAR_X = EVar(name='VarX', sort=SortApp('SortInt'))
VAR_Y = EVar(name='VarY', sort=SortApp('SortInt'))


class TestImpFuzz(KompiledTest):
KOMPILE_MAIN_FILE = K_FILES / 'imp.k'
KOMPILE_BACKEND = 'llvm'
SUBSTS = {VAR_X: kintegers(with_inj=KSort('AExp')), VAR_Y: kintegers(with_inj=KSort('AExp'))}

@staticmethod
def check(p: Pattern) -> None:
def check_inner(p: Pattern) -> Pattern:
match p:
case Assoc():
symbol = p.symbol()
args = (arg.top_down(check_inner) for arg in p.app.args)
return p.of(symbol, patterns=(p.app.let(args=args),))
case App("Lbl'UndsPipe'-'-GT-Unds'", args=(key, val)):
match key, val:
case (
App('inj', args=(DV(value=String('res')),)),
App('inj', args=(DV(value=String(resval)),)),
):
assert resval == '0'

return p

p.top_down(check_inner)

@staticmethod
def setup_program(definition_dir: Path, text: str) -> Pattern:
kore_text = _kast(definition_dir=definition_dir, input='program', output='kore', expression=text).stdout

program_pattern = KoreParser(kore_text).pattern()

def replace_var_ids(p: Pattern) -> Pattern:
match p:
case App('inj', _, (DV(_, String('varx')),)):
return VAR_X
case App('inj', _, (DV(_, String('vary')),)):
return VAR_Y
return p

program_pattern = program_pattern.top_down(replace_var_ids)
init_pattern = top_cell_initializer(
{
'$PGM': inj(SortApp('SortPgm'), SortApp('SortKItem'), program_pattern),
}
)

return init_pattern

def test_fuzz(
self,
definition_dir: Path,
) -> None:
# Given
program_text = """
// Checks the commutativity of addition
int x, y, a, b, res;
x = varx;
y = vary;
a = x + y;
b = y + x;
if ((a <= b) && (b <= a)) { // a == b
res = 0;
} else {
res = 1;
}
"""

init_pattern = self.setup_program(definition_dir, program_text)
gtrepta marked this conversation as resolved.
Show resolved Hide resolved

# Then
fuzz(definition_dir, init_pattern, self.SUBSTS, self.check)

def test_fuzz_fail(
self,
definition_dir: Path,
) -> None:
# Given
program_text = """
// Checks that x <= y
int x, y, res;
x = varx;
y = vary;
if (x <= y) {
res = 0;
} else {
res = 1;
}
"""

init_pattern = self.setup_program(definition_dir, program_text)

# Then
with pytest.raises(AssertionError):
fuzz(definition_dir, init_pattern, self.SUBSTS, self.check)
Loading