Skip to content

Commit 548aeea

Browse files
komet prove options for advanced users (#64)
* implement `--extra-module` * implement `--view-node` and `--remove-node` * add progress display to `komet prove run` * Set Version: 0.1.57 --------- Co-authored-by: devops <[email protected]>
1 parent 2a143ed commit 548aeea

File tree

6 files changed

+134
-46
lines changed

6 files changed

+134
-46
lines changed

package/version

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.56
1+
0.1.57

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "komet"
7-
version = "0.1.56"
7+
version = "0.1.57"
88
description = "K tooling for the Soroban platform"
99
authors = [
1010
"Runtime Verification, Inc. <[email protected]>",

src/komet/kasmer.py

+35-26
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050

5151
from hypothesis.strategies import SearchStrategy
5252
from pyk.kast.inner import KInner
53+
from pyk.kast.outer import KFlatModule
5354
from pyk.kore.syntax import Pattern
5455
from pyk.proof import APRProof, EqualityProof
5556
from pyk.utils import BugReport
@@ -63,9 +64,11 @@ class Kasmer:
6364
"""Reads soroban contracts, and runs tests for them."""
6465

6566
definition: SorobanDefinition
67+
extra_module: KFlatModule | None
6668

67-
def __init__(self, definition: SorobanDefinition) -> None:
69+
def __init__(self, definition: SorobanDefinition, extra_module: KFlatModule | None = None) -> None:
6870
self.definition = definition
71+
self.extra_module = extra_module
6972

7073
def _which(self, cmd: str) -> Path:
7174
path_str = shutil.which(cmd)
@@ -290,7 +293,7 @@ def make_steps(*args: KInner) -> KInner:
290293

291294
claim, _ = cterm_build_claim(name, lhs, rhs)
292295

293-
return run_claim(name, claim, proof_dir, bug_report)
296+
return run_claim(name, claim, self.extra_module, proof_dir, bug_report)
294297

295298
def deploy_and_run(
296299
self, contract_wasm: Path, child_wasms: tuple[Path, ...], max_examples: int = 100, id: str | None = None
@@ -306,26 +309,13 @@ def deploy_and_run(
306309
Raises:
307310
AssertionError if any of the tests fail
308311
"""
309-
print(f'Processing contract: {contract_wasm.stem}')
310-
311-
bindings = self.contract_bindings(contract_wasm)
312-
has_init = 'init' in (b.name for b in bindings)
312+
test_bindings, has_init = self.read_bindings(contract_wasm, id)
313313

314314
contract_kast = self.kast_from_wasm(contract_wasm)
315315
child_kasts = tuple(self.kast_from_wasm(c) for c in child_wasms)
316316

317317
conf, subst = self.deploy_test(contract_kast, child_kasts, has_init)
318318

319-
test_bindings = [b for b in bindings if b.name.startswith('test_') and (id is None or b.name == id)]
320-
321-
if id is None:
322-
print(f'Discovered {len(test_bindings)} test functions:')
323-
elif not test_bindings:
324-
raise KeyError(f'Test function {id!r} not found.')
325-
else:
326-
print('Selected a single test function:')
327-
print()
328-
329319
failed: list[FuzzError] = []
330320
with FuzzProgress(test_bindings, max_examples) as progress:
331321
for task in progress.fuzz_tasks:
@@ -370,21 +360,23 @@ def deploy_and_prove(
370360
Raises:
371361
KSorobanError if a proof fails
372362
"""
373-
bindings = self.contract_bindings(contract_wasm)
374-
has_init = 'init' in (b.name for b in bindings)
363+
test_bindings, has_init = self.read_bindings(contract_wasm, id)
375364

376365
contract_kast = self.kast_from_wasm(contract_wasm)
377366
child_kasts = tuple(self.kast_from_wasm(c) for c in child_wasms)
378367

379368
conf, subst = self.deploy_test(contract_kast, child_kasts, has_init)
380369

381-
test_bindings = [b for b in bindings if b.name.startswith('test_') and (id is None or b.name == id)]
382-
383-
for binding in test_bindings:
384-
print(binding.name)
385-
proof = self.run_prove(conf, subst, binding, always_allocate, proof_dir, bug_report)
386-
if proof.status == ProofStatus.FAILED:
387-
raise KSorobanError(proof.summary)
370+
with FuzzProgress(test_bindings, 1) as progress:
371+
for task in progress.fuzz_tasks:
372+
task.start()
373+
proof = self.run_prove(conf, subst, task.binding, always_allocate, proof_dir, bug_report)
374+
if proof.status == ProofStatus.PASSED:
375+
task.advance()
376+
task.end()
377+
else:
378+
task.fail()
379+
raise KSorobanError(proof.summary)
388380

389381
def prove_raw(
390382
self,
@@ -411,10 +403,27 @@ def prove_raw(
411403
if proof.status == ProofStatus.FAILED:
412404
raise KSorobanError(proof)
413405
else:
414-
proof = run_claim(claim.label, claim, proof_dir, bug_report)
406+
proof = run_claim(claim.label, claim, self.extra_module, proof_dir, bug_report)
415407
if proof.status == ProofStatus.FAILED:
416408
raise KSorobanError(proof.summary)
417409

410+
def read_bindings(self, contract_wasm: Path, id: str | None) -> tuple[list[ContractBinding], bool]:
411+
print(f'Processing contract: {contract_wasm.stem}')
412+
413+
bindings = self.contract_bindings(contract_wasm)
414+
has_init = 'init' in (b.name for b in bindings)
415+
test_bindings = [b for b in bindings if b.name.startswith('test_') and (id is None or b.name == id)]
416+
417+
if id is None:
418+
print(f'Discovered {len(test_bindings)} test functions:')
419+
elif not test_bindings:
420+
raise KeyError(f'Test function {id!r} not found.')
421+
else:
422+
print('Selected a single test function:')
423+
print()
424+
425+
return test_bindings, has_init
426+
418427

419428
@dataclass(frozen=True)
420429
class ContractBinding:

src/komet/komet.py

+64-12
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@
2828
from collections.abc import Iterator
2929
from subprocess import CompletedProcess
3030

31+
from pyk.kast.outer import KFlatModule
3132
from pyk.utils import BugReport
3233

33-
34-
sys.setrecursionlimit(4000)
34+
sys.setrecursionlimit(8000)
3535

3636

3737
class Backend(Enum):
@@ -59,6 +59,7 @@ def main() -> None:
5959
dir_path=args.directory,
6060
wasm=wasm,
6161
id=args.id,
62+
extra_module=args.extra_module,
6263
always_allocate=args.always_allocate,
6364
proof_dir=args.proof_dir,
6465
bug_report=args.bug_report,
@@ -67,10 +68,24 @@ def main() -> None:
6768
assert args.proof_dir is not None
6869
_exec_prove_view(proof_dir=args.proof_dir, id=args.id)
6970

71+
if args.prove_command == 'view-node':
72+
assert args.proof_dir is not None
73+
assert args.id is not None
74+
assert args.node is not None
75+
_exec_prove_view_node(proof_dir=args.proof_dir, id=args.id, node=args.node)
76+
if args.prove_command == 'remove-node':
77+
assert args.proof_dir is not None
78+
assert args.id is not None
79+
assert args.node is not None
80+
_exec_prove_remove_node(proof_dir=args.proof_dir, id=args.id, node=args.node)
7081
elif args.command == 'prove-raw':
7182
assert args.claim_file is not None
7283
_exec_prove_raw(
73-
claim_file=args.claim_file, label=args.label, proof_dir=args.proof_dir, bug_report=args.bug_report
84+
claim_file=args.claim_file,
85+
label=args.label,
86+
extra_module=args.extra_module,
87+
proof_dir=args.proof_dir,
88+
bug_report=args.bug_report,
7489
)
7590

7691
raise AssertionError()
@@ -89,10 +104,11 @@ def _exec_prove_raw(
89104
*,
90105
claim_file: Path,
91106
label: str | None,
107+
extra_module: KFlatModule | None,
92108
proof_dir: Path | None,
93109
bug_report: BugReport | None = None,
94110
) -> None:
95-
kasmer = Kasmer(symbolic_definition)
111+
kasmer = Kasmer(symbolic_definition, extra_module)
96112
try:
97113
kasmer.prove_raw(claim_file, label, proof_dir, bug_report)
98114
exit(0)
@@ -157,12 +173,13 @@ def _exec_prove_run(
157173
dir_path: Path | None,
158174
wasm: Path | None,
159175
id: str | None,
176+
extra_module: KFlatModule | None,
160177
always_allocate: bool,
161178
proof_dir: Path | None,
162179
bug_report: BugReport | None = None,
163180
) -> None:
164181
dir_path = Path.cwd() if dir_path is None else dir_path
165-
kasmer = Kasmer(symbolic_definition)
182+
kasmer = Kasmer(symbolic_definition, extra_module)
166183

167184
child_wasms: tuple[Path, ...] = ()
168185

@@ -202,6 +219,20 @@ def _exec_prove_view(*, proof_dir: Path, id: str) -> None:
202219
sys.exit(0)
203220

204221

222+
def _exec_prove_view_node(*, proof_dir: Path, id: str, node: int) -> None:
223+
proof = APRProof.read_proof_data(proof_dir, id)
224+
config = proof.kcfg.node(node).cterm.config
225+
print(symbolic_definition.krun.pretty_print(config))
226+
sys.exit(0)
227+
228+
229+
def _exec_prove_remove_node(*, proof_dir: Path, id: str, node: int) -> None:
230+
proof = APRProof.read_proof_data(proof_dir, id)
231+
proof.prune(node)
232+
proof.write_proof_data()
233+
sys.exit(0)
234+
235+
205236
@contextmanager
206237
def _preprocessed(program: Path) -> Iterator[Path]:
207238
program_text = program.read_text()
@@ -219,6 +250,14 @@ def _exit_with_output(cp: CompletedProcess) -> None:
219250
sys.exit(status)
220251

221252

253+
def extra_module_arg(extra_module: str) -> KFlatModule:
254+
extra_module_file, extra_module_name, *_ = extra_module.split(':')
255+
extra_module_path = Path(extra_module_file)
256+
if not extra_module_path.is_file():
257+
raise ValueError(f'Supplied --extra-module path is not a file: {extra_module_path}')
258+
return symbolic_definition.parse_lemmas_module(extra_module_path, extra_module_name)
259+
260+
222261
def _argument_parser() -> ArgumentParser:
223262
parser = ArgumentParser(prog='komet')
224263
command_parser = parser.add_subparsers(dest='command', required=True)
@@ -243,24 +282,22 @@ def _argument_parser() -> ArgumentParser:
243282
prove_parser.add_argument(
244283
'prove_command',
245284
default='run',
246-
choices=('run', 'view'),
285+
choices=('run', 'view', 'view-node', 'remove-node'),
247286
metavar='COMMAND',
248287
help='Proof command to run. One of (%(choices)s)',
249288
)
250-
prove_parser.add_argument('--proof-dir', type=ensure_dir_path, default=None, help='Output directory for proofs')
251-
prove_parser.add_argument('--bug-report', type=bug_report_arg, default=None, help='Bug report directory for proofs')
289+
prove_parser.add_argument('--node', type=int)
290+
_add_common_prove_arguments(prove_parser)
291+
252292
_add_common_test_arguments(prove_parser)
253293

254294
prove_raw_parser = command_parser.add_parser(
255295
'prove-raw',
256296
help='Prove K claims directly from a file, bypassing the usual test contract structure; intended for development and advanced users.',
257297
)
258298
prove_raw_parser.add_argument('claim_file', metavar='CLAIM_FILE', type=file_path, help='path to claim file')
259-
prove_raw_parser.add_argument('--proof-dir', type=ensure_dir_path, default=None, help='Output directory for proofs')
260-
prove_raw_parser.add_argument(
261-
'--bug-report', type=bug_report_arg, default=None, help='Bug report directory for proofs'
262-
)
263299
prove_raw_parser.add_argument('--label', help='Label of the K claim in the file')
300+
_add_common_prove_arguments(prove_raw_parser)
264301

265302
return parser
266303

@@ -280,3 +317,18 @@ def _add_common_test_arguments(parser: ArgumentParser) -> None:
280317
default=None,
281318
help='The working directory for the command (defaults to the current working directory).',
282319
)
320+
321+
322+
def _add_common_prove_arguments(parser: ArgumentParser) -> None:
323+
parser.add_argument('--proof-dir', type=ensure_dir_path, default=None, help='Output directory for proofs')
324+
parser.add_argument('--bug-report', type=bug_report_arg, default=None, help='Bug report directory for proofs')
325+
parser.add_argument(
326+
'--extra-module',
327+
dest='extra_module',
328+
default=None,
329+
type=extra_module_arg,
330+
help=(
331+
'Extra module with user-defined lemmas to include for verification (which must import KASMER module).'
332+
'Format is <file>:<module name>.'
333+
),
334+
)

src/komet/proof.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from pathlib import Path
1919

2020
from pyk.kast.inner import KInner
21-
from pyk.kast.outer import KClaim
21+
from pyk.kast.outer import KClaim, KFlatModule
2222
from pyk.utils import BugReport
2323

2424

@@ -37,14 +37,20 @@ def _explore_context(id: str, bug_report: BugReport | None) -> Iterator[KCFGExpl
3737
class SorobanSemantics(DefaultSemantics): ...
3838

3939

40-
def run_claim(id: str, claim: KClaim, proof_dir: Path | None = None, bug_report: BugReport | None = None) -> APRProof:
40+
def run_claim(
41+
id: str,
42+
claim: KClaim,
43+
extra_module: KFlatModule | None = None,
44+
proof_dir: Path | None = None,
45+
bug_report: BugReport | None = None,
46+
) -> APRProof:
4147
if proof_dir is not None and APRProof.proof_data_exists(id, proof_dir):
4248
proof = APRProof.read_proof_data(proof_dir, id)
4349
else:
4450
proof = APRProof.from_claim(symbolic_definition.kdefinition, claim=claim, logs={}, proof_dir=proof_dir)
4551

4652
with _explore_context(id, bug_report) as kcfg_explore:
47-
prover = APRProver(kcfg_explore)
53+
prover = APRProver(kcfg_explore, extra_module=extra_module)
4854
prover.advance_proof(proof)
4955

5056
proof.write_proof_data()

src/komet/utils.py

+24-3
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
from __future__ import annotations
22

3+
import logging
34
from functools import cached_property
5+
from subprocess import CalledProcessError
46
from typing import TYPE_CHECKING
57

6-
from pyk.kast.outer import read_kast_definition
8+
from pyk.kast.outer import KRule, read_kast_definition
79
from pyk.kdist import kdist
810
from pyk.konvert import kast_to_kore
911
from pyk.ktool.kompile import DefinitionInfo
1012
from pyk.ktool.kprove import KProve
1113
from pyk.ktool.krun import KRun
14+
from pyk.utils import single
1215

1316
if TYPE_CHECKING:
1417
from pathlib import Path
1518
from subprocess import CompletedProcess
16-
from typing import Any
19+
from typing import Any, Final
1720

1821
from pyk.kast.inner import KInner, KSort
19-
from pyk.kast.outer import KDefinition
22+
from pyk.kast.outer import KDefinition, KFlatModule
2023
from pyk.ktool.kompile import KompileBackend
2124

25+
_LOGGER: Final = logging.getLogger(__name__)
26+
2227

2328
class KSorobanError(RuntimeError): ...
2429

@@ -67,6 +72,22 @@ def krun_with_kast(self, pgm: KInner, sort: KSort | None = None, **kwargs: Any)
6772
kore_term = kast_to_kore(self.kdefinition, pgm, sort=sort)
6873
return self.krun.run_process(kore_term, **kwargs)
6974

75+
def parse_lemmas_module(self, module_path: Path, module_name: str) -> KFlatModule:
76+
try:
77+
modules = self.kprove.parse_modules(module_path, module_name=module_name)
78+
except CalledProcessError as e:
79+
_LOGGER.error('Could not parse extra module:')
80+
_LOGGER.error(e.stderr)
81+
raise e
82+
83+
module = single(module for module in modules.modules if module.name == module_name)
84+
85+
non_rule_sentences = [sent for sent in module.sentences if not isinstance(sent, KRule)]
86+
if non_rule_sentences:
87+
raise ValueError(f'Supplied --extra-module contains non-Rule sentences: {non_rule_sentences}')
88+
89+
return module
90+
7091

7192
concrete_definition = SorobanDefinition(kdist.get('soroban-semantics.llvm'))
7293
library_definition = SorobanDefinition(kdist.get('soroban-semantics.llvm-library'))

0 commit comments

Comments
 (0)