Skip to content

Commit 233b1e6

Browse files
authored
Add tests for K definitions (#39)
1 parent 95665c8 commit 233b1e6

File tree

7 files changed

+260
-11
lines changed

7 files changed

+260
-11
lines changed

.github/workflows/test-pr.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ jobs:
3939
uses: ./.github/actions/with-docker
4040
with:
4141
container-name: ${CONTAINER}
42-
- name: 'Build and run integration tests'
42+
- name: 'Build K definitions'
43+
run: docker exec -u user ${CONTAINER} make kdist
44+
- name: 'Run integration tests'
4345
run: docker exec -u user ${CONTAINER} make cov-integration
4446
- name: 'Tear down Docker container'
4547
if: always()

package/smoke-test.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ set -euxo pipefail
44

55
kimp --help
66

7-
kimp run --verbose examples/sumto10.imp --env 'x=0,y=1' --env z=2
7+
kimp run --verbose examples/sumto10.imp --env 'x=0,y=1' --env b1=false --env b2=true
88

99
kimp prove --verbose examples/specs/imp-sum-spec.k IMP-SUM-SPEC sum-spec
1010

src/kimp/__main__.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,16 @@ def create_argument_parser() -> ArgumentParser:
132132

133133
# Run
134134
def env(s: str) -> list[tuple[str, int]]:
135-
return [(var.strip(), int(val)) for var, val in (assign.split('=') for assign in s.split(','))]
135+
def parse(s: str) -> int:
136+
match s:
137+
case 'true':
138+
return True
139+
case 'false':
140+
return False
141+
case _:
142+
return int(s)
143+
144+
return [(var.strip(), parse(val)) for var, val in (assign.split('=') for assign in s.split(','))]
136145

137146
run_subparser = command_parser.add_parser('run', help='Run an IMP program', parents=[shared_args])
138147
run_subparser.add_argument('input_file', metavar='INPUT_FILE', type=file_path, help='Path to .imp file')

src/kimp/kimp.py

+52-6
Original file line numberDiff line numberDiff line change
@@ -182,15 +182,20 @@ def run(
182182
return llvm_interpret(definition_dir=self.dist.llvm_dir, pattern=pattern, depth=depth)
183183

184184
def pattern(self, *, pgm: str, env: Mapping[str, int]) -> Pattern:
185-
from pyk.kore.prelude import ID, INT, SORT_K_ITEM, inj, map_pattern, top_cell_initializer
185+
from pyk.kore.prelude import BOOL, ID, INT, SORT_K_ITEM, bool_dv, inj, int_dv, map_pattern, top_cell_initializer
186186
from pyk.kore.syntax import DV, SortApp, String
187187

188+
def inj_dv(val: int) -> Pattern:
189+
if isinstance(val, bool):
190+
return inj(BOOL, SORT_K_ITEM, bool_dv(val))
191+
return inj(INT, SORT_K_ITEM, int_dv(val))
192+
188193
pgm_pattern = self.parse(pgm)
189194
env_pattern = map_pattern(
190195
*(
191196
(
192197
inj(ID, SORT_K_ITEM, DV(ID, String(var))),
193-
inj(INT, SORT_K_ITEM, DV(INT, String(str(val)))),
198+
inj_dv(val),
194199
)
195200
for var, val in env.items()
196201
)
@@ -203,20 +208,61 @@ def pattern(self, *, pgm: str, env: Mapping[str, int]) -> Pattern:
203208
)
204209

205210
def parse(self, pgm: str) -> Pattern:
211+
from subprocess import CalledProcessError
212+
206213
from pyk.kore.parser import KoreParser
207214
from pyk.utils import run_process_2
208215

209216
parser = self.dist.llvm_dir / 'parser_PGM'
210217
args = [str(parser), '/dev/stdin']
211218

212-
kore_text = run_process_2(args, input=pgm).stdout
219+
try:
220+
kore_text = run_process_2(args, input=pgm).stdout
221+
except CalledProcessError as err:
222+
raise ValueError(err.stderr) from err
223+
213224
return KoreParser(kore_text).pattern()
214225

215226
def pretty(self, pattern: Pattern, color: bool | None = None) -> str:
216227
from pyk.kore.tools import kore_print
217228

218229
return kore_print(pattern, definition_dir=self.dist.llvm_dir, color=bool(color))
219230

231+
def env(self, pattern: Pattern) -> dict[str, int]:
232+
import pyk.kore.match as km
233+
from pyk.kore.prelude import BOOL, INT
234+
from pyk.utils import case, chain
235+
236+
extract = (
237+
chain
238+
>> km.app("Lbl'-LT-'generatedTop'-GT-'")
239+
>> km.arg("Lbl'-LT-'env'-GT-'")
240+
>> km.arg(0)
241+
>> km.kore_map_of(
242+
key=chain >> km.inj >> km.kore_id,
243+
value=chain
244+
>> km.match_inj
245+
>> case(
246+
(
247+
(
248+
lambda inj: inj.sorts[0] == BOOL,
249+
chain >> km.arg(0) >> km.kore_bool,
250+
),
251+
(
252+
lambda inj: inj.sorts[0] == INT,
253+
chain >> km.arg(0) >> km.kore_int,
254+
),
255+
)
256+
),
257+
)
258+
)
259+
260+
try:
261+
return dict(extract(pattern))
262+
except Exception as err:
263+
pretty_pattern = self.pretty(pattern)
264+
raise ValueError(f'Cannot extract environment from pattern:\n{pretty_pattern}') from err
265+
220266
def debug(self, pattern: Pattern) -> Callable[[int | None], None]:
221267
"""Return a closure that enables step-by-step debugging in a REPL.
222268
@@ -227,12 +273,12 @@ def debug(self, pattern: Pattern) -> Callable[[int | None], None]:
227273
step() # Run a single step
228274
step(1) # Run a single step
229275
step(0) # Just print the current configuration
230-
step(bound=None) # Run to completion
276+
step(depth=None) # Run to completion
231277
"""
232278

233-
def step(bound: int | None = 1) -> None:
279+
def step(depth: int | None = 1) -> None:
234280
nonlocal pattern
235-
pattern = self.run(pattern, depth=bound)
281+
pattern = self.run(pattern, depth=depth)
236282
print(self.pretty(pattern, color=True))
237283

238284
return step

src/tests/integration/test_expr.py

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Final
4+
5+
import pytest
6+
from pyk.ktool.krun import llvm_interpret
7+
8+
if TYPE_CHECKING:
9+
from pathlib import Path
10+
11+
from pyk.kore.syntax import Pattern
12+
13+
14+
@pytest.fixture(scope='module')
15+
def definition_dir() -> Path:
16+
from pyk.kdist import kdist
17+
18+
return kdist.get('imp-semantics.expr')
19+
20+
21+
TEST_DATA: Final = (
22+
('false', False),
23+
('true', True),
24+
('0', 0),
25+
('-1', -1),
26+
('--1', 1),
27+
('1 + 2', 3),
28+
('3 - 2', 1),
29+
('2 * 3', 6),
30+
('6 / 2', 3),
31+
('1 + 2 - 3', 0),
32+
('2 * 3 / 4', 1),
33+
('1 + 2 * 3', 7),
34+
('(1 + 2) * 3', 9),
35+
('0 == 0', True),
36+
('0 == 1', False),
37+
('true == true', True),
38+
('false == false', True),
39+
('true == false', False),
40+
('false == true', False),
41+
('1 >= 1', True),
42+
('1 > 1', False),
43+
('1 <= 1', True),
44+
('1 < 1', False),
45+
('0 < 1 == 1 < 2', True),
46+
('1 == 1 == true', True),
47+
('!true', False),
48+
('!false', True),
49+
('!!true', True),
50+
('!(1 > 2)', True),
51+
('false && true', False),
52+
('true && true', True),
53+
('true && 1', 1),
54+
('true || false', True),
55+
('false || false', False),
56+
('false || 1', 1),
57+
('1 > 2 && 1 == 1', False),
58+
('1 > 2 || 1 == 1', True),
59+
('true || false == false', True),
60+
('(true || false) == false', False),
61+
)
62+
63+
64+
@pytest.mark.parametrize('text,expected', TEST_DATA, ids=[test_id for test_id, _ in TEST_DATA])
65+
def test_expr(
66+
text: str,
67+
expected: int | bool,
68+
definition_dir: Path,
69+
) -> None:
70+
# When
71+
pgm = parse(definition_dir, text)
72+
pattern = config(pgm)
73+
result = llvm_interpret(definition_dir, pattern)
74+
actual = extract(definition_dir, result)
75+
76+
# Then
77+
assert actual == expected
78+
79+
80+
def parse(definition_dir: Path, text: str) -> Pattern:
81+
from subprocess import CalledProcessError
82+
83+
from pyk.kore.parser import KoreParser
84+
from pyk.utils import run_process_2
85+
86+
parser = definition_dir / 'parser_PGM'
87+
args = [str(parser), '/dev/stdin']
88+
89+
try:
90+
kore_text = run_process_2(args, input=text).stdout
91+
except CalledProcessError as err:
92+
raise ValueError(err.stderr) from err
93+
94+
return KoreParser(kore_text).pattern()
95+
96+
97+
def config(pgm: Pattern) -> Pattern:
98+
from pyk.kore.prelude import SORT_K_ITEM, inj, top_cell_initializer
99+
from pyk.kore.syntax import SortApp
100+
101+
return top_cell_initializer(
102+
{
103+
'$PGM': inj(SortApp('SortExpr'), SORT_K_ITEM, pgm),
104+
}
105+
)
106+
107+
108+
def extract(definition_dir: Path, pattern: Pattern) -> int | bool:
109+
from pyk.kore.syntax import DV, App, String
110+
111+
match pattern:
112+
case App(
113+
"Lbl'-LT-'generatedTop'-GT-'",
114+
args=(
115+
App(
116+
"Lbl'-LT-'k'-GT-'",
117+
args=(
118+
App(
119+
'kseq',
120+
args=(
121+
App('inj', args=(DV(value=String(res)),)),
122+
App('dotk'),
123+
),
124+
),
125+
),
126+
),
127+
*_,
128+
),
129+
):
130+
try:
131+
return int(res)
132+
except Exception:
133+
pass
134+
match res:
135+
case 'true':
136+
return True
137+
case 'false':
138+
return False
139+
140+
pretty_pattern = pretty(definition_dir, pattern)
141+
raise ValueError(f'Cannot extract result from pattern:\n{pretty_pattern}')
142+
143+
144+
def pretty(definition_dir: Path, pattern: Pattern) -> str:
145+
from pyk.kore.tools import kore_print
146+
147+
return kore_print(pattern, definition_dir=definition_dir)

src/tests/integration/test_imp.py

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from __future__ import annotations
2+
3+
from itertools import count
4+
from typing import TYPE_CHECKING
5+
6+
import pytest
7+
8+
from kimp import KImp
9+
10+
if TYPE_CHECKING:
11+
from typing import Final
12+
13+
Env = dict[str, int]
14+
15+
16+
TEST_DATA: Final[tuple[tuple[Env, str, Env], ...]] = (
17+
({}, '{}', {}),
18+
({'x': 0}, '{}', {'x': 0}),
19+
({}, 'x = 1;', {'x': 1}),
20+
({}, 'x = 1 + 1;', {'x': 2}),
21+
({'x': 0}, 'x = 1;', {'x': 1}),
22+
({'x': 0, 'y': 1}, 'x = y;', {'x': 1, 'y': 1}),
23+
({'x': 1}, 'x = false;', {'x': False}),
24+
({}, '{ x = 0; }', {'x': 0}),
25+
({}, 'x = 0; x = 1;', {'x': 1}),
26+
({}, 'x = 0; y = 1;', {'x': 0, 'y': 1}),
27+
({'x': 0, 'y': 1}, 'z = x; x = y; y = z;', {'x': 1, 'y': 0, 'z': 0}),
28+
({'b': True}, 'if (b) x = 1;', {'b': True, 'x': 1}),
29+
({'b': False}, 'if (b) x = 1;', {'b': False}),
30+
({'b': True}, 'if (b) x = 1; else x = 2;', {'b': True, 'x': 1}),
31+
({'b': False}, 'if (b) x = 1; else x = 2;', {'b': False, 'x': 2}),
32+
({'x': 2}, 'while (x > 0) x = x - 1;', {'x': 0}),
33+
)
34+
35+
36+
@pytest.mark.parametrize('env,pgm,expected', TEST_DATA, ids=count())
37+
def test_kimp(env: Env, pgm: str, expected: Env) -> None:
38+
# Given
39+
kimp = KImp()
40+
41+
# When
42+
pattern = kimp.pattern(pgm=pgm, env=env)
43+
result = kimp.run(pattern, depth=1000)
44+
actual = kimp.env(result)
45+
46+
# Then
47+
assert actual == expected

src/tests/integration/test_integration.py

-2
This file was deleted.

0 commit comments

Comments
 (0)