Skip to content

Commit c699da5

Browse files
authored
Implement caching for SMIR kompilation (#777)
* Upon SMIR kompilation, save the digest of the `smir.json` file into a file `smir-digest.json` in the target directory. This is then used on subsequent calls to check if re-kompilation is necessary. * Add option `--target-dir` for `kmir run`.
1 parent 5a2677c commit c699da5

File tree

7 files changed

+111
-53
lines changed

7 files changed

+111
-53
lines changed

kmir/src/kmir/__main__.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from pyk.proof.show import APRProofShow
1515
from pyk.proof.tui import APRProofViewer
1616

17-
from .build import HASKELL_DEF_DIR, LLVM_DEF_DIR, LLVM_LIB_DIR
17+
from .build import HASKELL_DEF_DIR, LLVM_LIB_DIR
1818
from .cargo import CargoProject
1919
from .kmir import KMIR, KMIRAPRNodePrinter
2020
from .linker import link
@@ -42,8 +42,6 @@
4242

4343

4444
def _kmir_run(opts: RunOpts) -> None:
45-
kmir = KMIR(HASKELL_DEF_DIR) if opts.haskell_backend else KMIR(LLVM_DEF_DIR)
46-
4745
if opts.file:
4846
smir_info = SMIRInfo.from_file(Path(opts.file))
4947
else:
@@ -54,11 +52,17 @@ def _kmir_run(opts: RunOpts) -> None:
5452
# target = opts.bin if opts.bin else cargo.default_target
5553
smir_info = cargo.smir_for_project(clean=False)
5654

57-
with tempfile.TemporaryDirectory() as work_dir:
58-
kmir = KMIR.from_kompiled_kore(smir_info, symbolic=opts.haskell_backend, target_dir=work_dir)
55+
def run(target_dir: Path):
56+
kmir = KMIR.from_kompiled_kore(smir_info, symbolic=opts.haskell_backend, target_dir=target_dir)
5957
result = kmir.run_smir(smir_info, start_symbol=opts.start_symbol, depth=opts.depth)
6058
print(kmir.kore_to_pretty(result))
6159

60+
if opts.target_dir:
61+
run(target_dir=opts.target_dir)
62+
else:
63+
with tempfile.TemporaryDirectory() as target_dir:
64+
run(target_dir=Path(target_dir))
65+
6266

6367
def _kmir_prove_rs(opts: ProveRSOpts) -> None:
6468
proof = KMIR.prove_rs(opts)
@@ -185,6 +189,7 @@ def _arg_parser() -> ArgumentParser:
185189
run_target_selection = run_parser.add_mutually_exclusive_group()
186190
run_target_selection.add_argument('--bin', metavar='TARGET', help='Target to run')
187191
run_target_selection.add_argument('--file', metavar='SMIR', help='SMIR json file to execute')
192+
run_parser.add_argument('--target-dir', type=Path, metavar='TARGET_DIR', help='SMIR kompilation target directory')
188193
run_parser.add_argument('--depth', type=int, metavar='DEPTH', help='Depth to execute')
189194
run_parser.add_argument(
190195
'--start-symbol', type=str, metavar='SYMBOL', default='main', help='Symbol name to begin execution from'
@@ -313,6 +318,7 @@ def _parse_args(ns: Namespace) -> KMirOpts:
313318
return RunOpts(
314319
bin=ns.bin,
315320
file=ns.file,
321+
target_dir=ns.target_dir,
316322
depth=ns.depth,
317323
start_symbol=ns.start_symbol,
318324
haskell_backend=ns.haskell_backend,

kmir/src/kmir/kmir.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __init__(
5656

5757
@staticmethod
5858
def from_kompiled_kore(
59-
smir_info: SMIRInfo, target_dir: str, bug_report: Path | None = None, symbolic: bool = True
59+
smir_info: SMIRInfo, target_dir: Path, bug_report: Path | None = None, symbolic: bool = True
6060
) -> KMIR:
6161
from .kompile import kompile_smir
6262

@@ -147,12 +147,9 @@ def prove_rs(opts: ProveRSOpts) -> APRProof:
147147
_LOGGER.info(f'Reading proof from disc: {opts.proof_dir}, {label}')
148148
apr_proof = APRProof.read_proof_data(opts.proof_dir, label)
149149

150-
# TODO avoid compilation, use compilation output from the proof directory
151-
# kmir = KMIR(opts.proof_dir / label / haskell, opts.proof_dir / label / llvm-library) if they exist
152-
# or else implement this in the `from_kompiled_kore` constructor
153150
smir_info = SMIRInfo.from_file(target_path / 'smir.json')
154151
kmir = KMIR.from_kompiled_kore(
155-
smir_info, symbolic=True, bug_report=opts.bug_report, target_dir=str(target_path)
152+
smir_info, symbolic=True, bug_report=opts.bug_report, target_dir=target_path
156153
)
157154
else:
158155
_LOGGER.info(f'Constructing initial proof: {label}')
@@ -176,7 +173,7 @@ def prove_rs(opts: ProveRSOpts) -> APRProof:
176173
_LOGGER.debug(f'Missing-body function symbols (first 5): {missing_body_syms[:5]}')
177174

178175
kmir = KMIR.from_kompiled_kore(
179-
smir_info, symbolic=True, bug_report=opts.bug_report, target_dir=str(target_path)
176+
smir_info, symbolic=True, bug_report=opts.bug_report, target_dir=target_path
180177
)
181178

182179
apr_proof = kmir.apr_proof_from_smir(

kmir/src/kmir/kompile.py

Lines changed: 82 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from __future__ import annotations
22

3+
import json
34
import logging
45
import shutil
56
from abc import ABC, abstractmethod
67
from dataclasses import dataclass
7-
from pathlib import Path
88
from typing import TYPE_CHECKING
99

1010
from pyk.kast.inner import KApply, KSort
@@ -15,6 +15,7 @@
1515
from .kmir import KMIR
1616

1717
if TYPE_CHECKING:
18+
from pathlib import Path
1819
from typing import Any, Final
1920

2021
from pyk.kast import KInner
@@ -54,45 +55,74 @@ def create_kmir(self, *, bug_report_file: Path | None = None) -> KMIR:
5455
)
5556

5657

58+
@dataclass
59+
class KompileDigest:
60+
digest: str
61+
symbolic: bool
62+
63+
@staticmethod
64+
def load(target_dir: Path) -> KompileDigest:
65+
digest_file = KompileDigest._digest_file(target_dir)
66+
67+
if not digest_file.exists():
68+
raise ValueError(f'Digest file does not exist: {digest_file}')
69+
70+
data = json.loads(digest_file.read_text())
71+
return KompileDigest(
72+
digest=data['digest'],
73+
symbolic=data['symbolic'],
74+
)
75+
76+
def write(self, target_dir: Path) -> None:
77+
self._digest_file(target_dir).write_text(
78+
json.dumps(
79+
{
80+
'digest': self.digest,
81+
'symbolic': self.symbolic,
82+
},
83+
),
84+
)
85+
86+
@staticmethod
87+
def _digest_file(target_dir: Path) -> Path:
88+
return target_dir / 'smir-digest.json'
89+
90+
5791
def kompile_smir(
5892
smir_info: SMIRInfo,
59-
target_dir: str,
93+
target_dir: Path,
6094
bug_report: Path | None = None,
6195
symbolic: bool = True,
6296
) -> KompiledSMIR:
63-
kmir = KMIR(HASKELL_DEF_DIR)
64-
65-
def _insert_rules_and_write(input_file: Path, rules: list[str], output_file: Path) -> None:
66-
with open(input_file, 'r') as f:
67-
lines = f.readlines()
97+
kompile_digest: KompileDigest | None = None
98+
try:
99+
kompile_digest = KompileDigest.load(target_dir)
100+
except Exception:
101+
pass
68102

69-
# last line must start with 'endmodule'
70-
last_line = lines[-1]
71-
assert last_line.startswith('endmodule')
72-
new_lines = lines[:-1]
103+
target_hs_path = target_dir / 'haskell'
104+
target_llvm_lib_path = target_dir / 'llvm-library'
105+
target_llvm_path = target_dir / 'llvm'
73106

74-
# Insert rules before the endmodule line
75-
new_lines.append(f'\n// Generated from file {input_file}\n\n')
76-
new_lines.extend(rules)
77-
new_lines.append('\n' + last_line)
107+
if kompile_digest is not None and kompile_digest.digest == smir_info.digest and kompile_digest.symbolic == symbolic:
108+
_LOGGER.info(f'Kompiled SMIR up-to-date, no kompilation necessary: {target_dir}')
109+
if symbolic:
110+
return KompiledSymbolic(haskell_dir=target_hs_path, llvm_lib_dir=target_llvm_lib_path)
111+
else:
112+
return KompiledConcrete(llvm_dir=target_llvm_path)
78113

79-
# Write to output file
80-
with open(output_file, 'w') as f:
81-
f.writelines(new_lines)
114+
_LOGGER.info(f'Kompiling SMIR program: {target_dir}')
82115

83-
target_path = Path(target_dir)
84-
# TODO if target dir exists and contains files, check file dates (definition files and interpreter)
85-
# to decide whether or not to recompile. For now we always recompile.
86-
target_path.mkdir(parents=True, exist_ok=True)
116+
kompile_digest = KompileDigest(digest=smir_info.digest, symbolic=symbolic)
117+
target_dir.mkdir(parents=True, exist_ok=True)
87118

119+
kmir = KMIR(HASKELL_DEF_DIR)
88120
rules = _make_kore_rules(kmir, smir_info)
89121
_LOGGER.info(f'Generated {len(rules)} function equations to add to `definition.kore')
90122

91123
if symbolic:
92124
# Create output directories
93-
target_llvm_path = target_path / 'llvm-library'
94-
target_llvmdt_path = target_llvm_path / 'dt'
95-
target_hs_path = target_path / 'haskell'
125+
target_llvmdt_path = target_llvm_lib_path / 'dt'
96126

97127
_LOGGER.info(f'Creating directories {target_llvmdt_path} and {target_hs_path}')
98128
target_llvmdt_path.mkdir(parents=True, exist_ok=True)
@@ -101,7 +131,7 @@ def _insert_rules_and_write(input_file: Path, rules: list[str], output_file: Pat
101131
# Process LLVM definition
102132
_LOGGER.info('Writing LLVM definition file')
103133
llvm_def_file = LLVM_LIB_DIR / 'definition.kore'
104-
llvm_def_output = target_llvm_path / 'definition.kore'
134+
llvm_def_output = target_llvm_lib_path / 'definition.kore'
105135
_insert_rules_and_write(llvm_def_file, rules, llvm_def_output)
106136

107137
# Run llvm-kompile-matching and llvm-kompile for LLVM
@@ -123,7 +153,7 @@ def _insert_rules_and_write(input_file: Path, rules: list[str], output_file: Pat
123153
'-O2',
124154
'--',
125155
'-o',
126-
target_llvm_path / 'interpreter',
156+
target_llvm_lib_path / 'interpreter',
127157
],
128158
check=True,
129159
)
@@ -141,13 +171,11 @@ def _insert_rules_and_write(input_file: Path, rules: list[str], output_file: Pat
141171
shutil.copy2(file_path, target_hs_path / file_path.name)
142172
elif file_path.is_dir():
143173
shutil.copytree(file_path, target_hs_path / file_path.name, dirs_exist_ok=True)
144-
return KompiledSymbolic(
145-
haskell_dir=target_hs_path,
146-
llvm_lib_dir=target_llvm_path,
147-
)
148-
else:
149174

150-
target_llvm_path = target_path / 'llvm'
175+
kompile_digest.write(target_dir)
176+
return KompiledSymbolic(haskell_dir=target_hs_path, llvm_lib_dir=target_llvm_lib_path)
177+
178+
else:
151179
target_llvmdt_path = target_llvm_path / 'dt'
152180
_LOGGER.info(f'Creating directory {target_llvmdt_path}')
153181
target_llvmdt_path.mkdir(parents=True, exist_ok=True)
@@ -183,6 +211,8 @@ def _insert_rules_and_write(input_file: Path, rules: list[str], output_file: Pat
183211
for file in to_copy:
184212
_LOGGER.info(f'Copying file {file}')
185213
shutil.copy2(LLVM_DEF_DIR / file, target_llvm_path / file)
214+
215+
kompile_digest.write(target_dir)
186216
return KompiledConcrete(llvm_dir=target_llvm_path)
187217

188218

@@ -294,3 +324,22 @@ def _decode_alloc(smir_info: SMIRInfo, raw_alloc: Any) -> tuple[KInner, KInner]:
294324

295325
alloc_id_term = KApply('allocId', intToken(alloc_id))
296326
return alloc_id_term, value.to_kast()
327+
328+
329+
def _insert_rules_and_write(input_file: Path, rules: list[str], output_file: Path) -> None:
330+
with open(input_file, 'r') as f:
331+
lines = f.readlines()
332+
333+
# last line must start with 'endmodule'
334+
last_line = lines[-1]
335+
assert last_line.startswith('endmodule')
336+
new_lines = lines[:-1]
337+
338+
# Insert rules before the endmodule line
339+
new_lines.append(f'\n// Generated from file {input_file}\n\n')
340+
new_lines.extend(rules)
341+
new_lines.append('\n' + last_line)
342+
343+
# Write to output file
344+
with open(output_file, 'w') as f:
345+
f.writelines(new_lines)

kmir/src/kmir/options.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class KMirOpts(LoggingOptions): ...
2222
class RunOpts(KMirOpts):
2323
bin: str | None
2424
file: str | None
25+
target_dir: Path | None
2526
depth: int
2627
start_symbol: str
2728
haskell_backend: bool

kmir/src/kmir/smir.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ def dump(self, smir_json_file: Path) -> None:
4040
def name(self) -> str:
4141
return self._smir['name']
4242

43+
@cached_property
44+
def digest(self) -> str:
45+
import hashlib
46+
47+
hash_object = hashlib.sha256(str(self._smir).encode('UTF-8'))
48+
return hash_object.hexdigest()
49+
4350
@cached_property
4451
def allocs(self) -> dict[AllocId, AllocInfo]:
4552
return {

kmir/src/tests/integration/test_decode_value.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def definition_dir(): # -> Path:
3131
try:
3232
with open(lock_file, 'x') as _:
3333
# generate and compile an LLVM interpreter with the type-table
34-
_ = KMIR.from_kompiled_kore(TEST_SMIR, target_dir=str(target_dir), symbolic=False)
34+
_ = KMIR.from_kompiled_kore(TEST_SMIR, target_dir=target_dir, symbolic=False)
3535
lock_file.unlink()
3636
except FileExistsError:
3737
# wait loop until interpreter exists, max 1min

kmir/src/tests/integration/test_integration.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -329,16 +329,14 @@ def test_exec_smir(
329329
test_case: tuple[str, Path, Path, int],
330330
symbolic: bool,
331331
update_expected_output: bool,
332+
tmp_path: Path,
332333
) -> None:
333-
334-
(_, input_json, output_kast, depth) = test_case
334+
_, input_json, output_kast, depth = test_case
335335
smir_info = SMIRInfo.from_file(input_json)
336-
337-
with tempfile.TemporaryDirectory() as temp_dir:
338-
kmir_backend = KMIR.from_kompiled_kore(smir_info, target_dir=temp_dir, symbolic=symbolic)
339-
result = kmir_backend.run_smir(smir_info, depth=depth)
340-
result_pretty = kmir_backend.kore_to_pretty(result).rstrip()
341-
assert_or_update_show_output(result_pretty, output_kast, update=update_expected_output)
336+
kmir_backend = KMIR.from_kompiled_kore(smir_info, target_dir=tmp_path, symbolic=symbolic)
337+
result = kmir_backend.run_smir(smir_info, depth=depth)
338+
result_pretty = kmir_backend.kore_to_pretty(result).rstrip()
339+
assert_or_update_show_output(result_pretty, output_kast, update=update_expected_output)
342340

343341

344342
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)