Skip to content

Commit cf2eecc

Browse files
Construct KCFGs for Foundry proof obligations (#1372)
* kevm-pyk/utils.py: bring in KCFG_from_claim from downstream * kevm-pyk/utils.py: remove unused methods * kevm-pyk/utils: add KDefinition_claims method * kevm-pyk/__main__: generate KCFGs for each claim too * kevm-pyk/: correct reading in of KCFGs and writing of them * kevm-pyk/utils: bring in sanitize_config, refactor from_claim a bit * kevm-pyk/: discharge proofs using kcfgs instead of direct use of kprove * tests/foundry/exclude: remove passing tests * kevm-pyk/solc_to_k: abstract pre/post ACCOUNTS cell map rest * Revert "kevm-pyk/solc_to_k: abstract pre/post ACCOUNTS cell map rest" This reverts commit 3ecf607. * Revert "tests/foundry/exclude: remove passing tests" This reverts commit 77e9864. * kevm-pyk/__main__: make sure we apply --test filter * kevm-pyk/kevm: unparse .AccountCellMap as .Bag * Update kevm-pyk/src/kevm_pyk/__main__.py Co-authored-by: Tamás Tóth <[email protected]> Co-authored-by: Tamás Tóth <[email protected]>
1 parent e6086d5 commit cf2eecc

File tree

3 files changed

+159
-43
lines changed

3 files changed

+159
-43
lines changed

kevm-pyk/src/kevm_pyk/__main__.py

+82-19
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88

99
from pyk.cli_utils import dir_path, file_path
1010
from pyk.kast import KApply, KDefinition, KFlatModule, KImport, KRequire, KSort
11+
from pyk.kcfg import KCFG
1112
from pyk.ktool.krun import _krun
13+
from pyk.prelude import mlTop
1214

1315
from .gst_to_kore import gst_to_kore
1416
from .kevm import KEVM
1517
from .solc_to_k import Contract, contract_to_k, solc_compile
16-
from .utils import KPrint_make_unparsing, add_include_arg
18+
from .utils import KCFG_from_claim, KPrint_make_unparsing, add_include_arg, read_kast_flatmodulelist
1719

1820
_LOGGER: Final = logging.getLogger(__name__)
1921
_LOG_FORMAT: Final = '%(levelname)s %(asctime)s %(name)s - %(message)s'
@@ -177,6 +179,8 @@ def exec_foundry_kompile(
177179
md_selector: Optional[str],
178180
regen: bool = False,
179181
rekompile: bool = False,
182+
reparse: bool = False,
183+
reinit: bool = False,
180184
requires: Iterable[str] = (),
181185
imports: Iterable[str] = (),
182186
**kwargs,
@@ -189,6 +193,9 @@ def exec_foundry_kompile(
189193
spec_module = 'FOUNDRY-SPEC'
190194
foundry_definition_dir = foundry_out / 'kompiled'
191195
foundry_main_file = foundry_definition_dir / 'foundry.k'
196+
kompiled_timestamp = foundry_definition_dir / 'timestamp'
197+
parsed_spec = foundry_definition_dir / 'spec.json'
198+
kcfgs_file = foundry_definition_dir / 'kcfgs.json'
192199
requires = ['lemmas/lemmas.k', 'lemmas/int-simplification.k'] + list(requires)
193200
imports = ['LEMMAS', 'INT-SIMPLIFICATION'] + list(imports)
194201
if not foundry_definition_dir.exists():
@@ -205,7 +212,7 @@ def exec_foundry_kompile(
205212
imports=list(imports),
206213
output=fmf,
207214
)
208-
if regen or rekompile or not (foundry_definition_dir / 'timestamp').exists():
215+
if regen or rekompile or not kompiled_timestamp.exists():
209216
KEVM.kompile(
210217
foundry_definition_dir,
211218
foundry_main_file,
@@ -216,6 +223,26 @@ def exec_foundry_kompile(
216223
md_selector=md_selector,
217224
profile=profile,
218225
)
226+
kevm = KEVM(foundry_definition_dir, main_file=foundry_main_file, profile=profile)
227+
if regen or rekompile or reparse or not parsed_spec.exists():
228+
prove_args = add_include_arg(includes)
229+
kevm.prove(
230+
foundry_main_file,
231+
spec_module_name=spec_module,
232+
dry_run=True,
233+
args=(['--emit-json-spec', str(parsed_spec)] + prove_args),
234+
)
235+
if regen or rekompile or reparse or reinit or not kcfgs_file.exists():
236+
cfgs: Dict[str, Dict] = {}
237+
for module in read_kast_flatmodulelist(parsed_spec).modules:
238+
for claim in module.claims:
239+
cfg_label = claim.att["label"]
240+
_LOGGER.info(f'Producing KCFG: {cfg_label}')
241+
cfgs[cfg_label] = KCFG_from_claim(kevm.definition, claim).to_dict()
242+
with open(kcfgs_file, 'w') as kf:
243+
kf.write(json.dumps(cfgs))
244+
kf.close()
245+
_LOGGER.info(f'Wrote file: {kcfgs_file}')
219246

220247

221248
def exec_prove(
@@ -267,23 +294,45 @@ def exec_foundry_prove(
267294
_ignore_arg(kwargs, 'definition_dir', f'--definition: {kwargs["definition_dir"]}')
268295
_ignore_arg(kwargs, 'spec_module', f'--spec-module: {kwargs["spec_module"]}')
269296
definition_dir = foundry_out / 'kompiled'
270-
spec_file = definition_dir / 'foundry.k'
271-
spec_module = 'FOUNDRY-SPEC'
272-
claims = [Contract.contract_test_to_claim_id(_t) for _t in tests]
273-
exclude_claims = [Contract.contract_test_to_claim_id(_t) for _t in exclude_tests]
274-
exec_prove(
275-
definition_dir,
276-
profile,
277-
spec_file,
278-
includes=includes,
279-
debug_equations=debug_equations,
280-
bug_report=bug_report,
281-
depth=depth,
282-
claims=claims,
283-
exclude_claims=exclude_claims,
284-
spec_module=spec_module,
285-
**kwargs,
286-
)
297+
use_directory = foundry_out / 'specs'
298+
use_directory.mkdir(parents=True, exist_ok=True)
299+
kevm = KEVM(definition_dir, profile=profile, use_directory=use_directory)
300+
kcfgs_file = definition_dir / 'kcfgs.json'
301+
kcfgs: Dict[str, KCFG] = {}
302+
_LOGGER.info(f'Reading file: {kcfgs_file}')
303+
with open(kcfgs_file, 'r') as kf:
304+
kcfgs = {k: KCFG.from_dict(v) for k, v in json.loads(kf.read()).items()}
305+
tests = [Contract.contract_test_to_claim_id(_t) for _t in tests]
306+
exclude_tests = [Contract.contract_test_to_claim_id(_t) for _t in exclude_tests]
307+
claims = list(kcfgs.keys())
308+
_unfound_kcfgs: List[str] = []
309+
if len(tests) > 0:
310+
kcfgs = {k: kcfg for k, kcfg in kcfgs.items() if k in tests}
311+
for _t in tests:
312+
if _t not in claims:
313+
_unfound_kcfgs.append(_t)
314+
for _t in exclude_tests:
315+
if _t not in claims:
316+
_unfound_kcfgs.append(_t)
317+
if _t in kcfgs:
318+
kcfgs.pop(_t)
319+
if _unfound_kcfgs:
320+
_LOGGER.error(f'Missing KCFGs for tests: {_unfound_kcfgs}')
321+
sys.exit(1)
322+
_failed_claims: List[str] = []
323+
for kcfg_name, kcfg in kcfgs.items():
324+
_LOGGER.info(f'Proving KCFG: {kcfg_name}')
325+
edge = kcfg.create_edge(kcfg.get_unique_init().id, kcfg.get_unique_target().id, mlTop(), depth=-1)
326+
claim = edge.to_claim()
327+
result = kevm.prove_claim(claim, kcfg_name.replace('.', '-'))
328+
if type(result) is KApply and result.label.name == '#Top':
329+
_LOGGER.info(f'Proved KCFG: {kcfg_name}')
330+
else:
331+
_LOGGER.error(f'Failed to prove KCFG: {kcfg_name}')
332+
_failed_claims.append(kcfg_name)
333+
if _failed_claims:
334+
_LOGGER.error(f'Failed to prove KCFGs: {_failed_claims}')
335+
sys.exit(1)
287336

288337

289338
def exec_run(
@@ -482,6 +531,20 @@ def parse(s):
482531
action='store_true',
483532
help='Rekompile foundry.k even if kompiled definition already exists.',
484533
)
534+
foundry_kompile.add_argument(
535+
'--reparse',
536+
dest='reparse',
537+
default=False,
538+
action='store_true',
539+
help='Reparse K specifications even if the parsed spec already exists.',
540+
)
541+
foundry_kompile.add_argument(
542+
'--reinit',
543+
dest='reinit',
544+
default=False,
545+
action='store_true',
546+
help='Reinitialize kcfgs even if they already exist.',
547+
)
485548

486549
foundry_prove_args = command_parser.add_parser(
487550
'foundry-prove',

kevm-pyk/src/kevm_pyk/kevm.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def _patch_symbol_table(symbol_table: Dict[str, Any]) -> None:
8383
symbol_table['_|->_'] = paren(symbol_table['_|->_'])
8484
symbol_table['_Map_'] = paren(lambda m1, m2: m1 + '\n' + m2)
8585
symbol_table['_AccountCellMap_'] = paren(lambda a1, a2: a1 + '\n' + a2)
86-
symbol_table['.AccountCellMap'] = lambda: ''
86+
symbol_table['.AccountCellMap'] = lambda: '.Bag'
8787
symbol_table['AccountCellMapItem'] = lambda k, v: v
8888
symbol_table['_[_:=_]_EVM-TYPES_Memory_Memory_Int_ByteArray'] = lambda m, k, v: m + ' [ ' + k + ' := (' + v + '):ByteArray ]'
8989
symbol_table['_[_.._]_EVM-TYPES_ByteArray_ByteArray_Int_Int'] = lambda m, s, w: '(' + m + ' [ ' + s + ' .. ' + w + ' ]):ByteArray'

kevm-pyk/src/kevm_pyk/utils.py

+76-23
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,56 @@
1+
from pathlib import Path
12
from typing import Collection, Iterable, List
23

3-
from pyk.kast import KDefinition, KFlatModule, KImport, KTerminal, KVariable
4-
from pyk.kastManip import abstract_term_safely, is_anon_var, split_config_and_constraints, split_config_from, substitute
4+
from pyk.cfg_manager import instantiate_cell_vars, rename_generated_vars
5+
from pyk.cterm import CTerm
6+
from pyk.kast import KApply, KClaim, KDefinition, KFlatModule, KFlatModuleList, KImport, KInner, KVariable, read_kast
7+
from pyk.kastManip import (
8+
abstract_term_safely,
9+
bool_to_ml_pred,
10+
bottom_up,
11+
extract_lhs,
12+
extract_rhs,
13+
flatten_label,
14+
free_vars,
15+
is_anon_var,
16+
ml_pred_to_bool,
17+
remove_generated_cells,
18+
split_config_and_constraints,
19+
split_config_from,
20+
substitute,
21+
undo_aliases,
22+
)
23+
from pyk.kcfg import KCFG
524
from pyk.ktool import KPrint
25+
from pyk.prelude import Bool, mlAnd
26+
27+
28+
def read_kast_flatmodulelist(ifile: Path) -> KFlatModuleList:
29+
_flat_module_list = read_kast(ifile)
30+
assert isinstance(_flat_module_list, KFlatModuleList)
31+
return _flat_module_list
32+
33+
34+
def KCFG_from_claim(defn: KDefinition, claim: KClaim) -> KCFG: # noqa: N802
35+
def _make_cterm(_kinner: KInner, _cond: KInner) -> CTerm:
36+
_kinner = _kinner if _cond == Bool.true else mlAnd([_kinner, bool_to_ml_pred(_cond)])
37+
_kinner = sanitize_config(defn, _kinner)
38+
return CTerm(_kinner)
39+
40+
cfg = KCFG()
41+
claim_body = claim.body
42+
claim_body = instantiate_cell_vars(defn, claim_body)
43+
claim_body = rename_generated_vars(CTerm(claim_body)).kast
44+
45+
claim_lhs = _make_cterm(extract_lhs(claim_body), claim.requires)
46+
init_node = cfg.create_node(claim_lhs)
47+
cfg.add_init(init_node.id)
48+
49+
claim_rhs = _make_cterm(extract_rhs(claim_body), claim.ensures)
50+
target_node = cfg.create_node(claim_rhs)
51+
cfg.add_target(target_node.id)
52+
53+
return cfg
654

755

856
def KPrint_make_unparsing(_self: KPrint, extra_modules: Iterable[KFlatModule] = ()) -> KPrint: # noqa: N802
@@ -28,28 +76,33 @@ def abstract_cell_vars(cterm, keep_vars: Collection[KVariable] = ()):
2876
return substitute(config, subst)
2977

3078

31-
def get_productions(definition):
32-
return [prod for module in definition.modules for prod in module.productions]
79+
def sanitize_config(defn: KDefinition, init_term: KInner) -> KInner:
80+
def _var_name(vname):
81+
new_vname = vname
82+
while new_vname.startswith('_') or new_vname.startswith('?'):
83+
new_vname = new_vname[1:]
84+
return new_vname
85+
86+
free_vars_subst = {vname: KVariable(_var_name(vname)) for vname in free_vars(init_term)}
3387

88+
# TODO: This is somewhat hacky. We shouldn't have to touch the config this much.
89+
# Likely, the frontend should just be giving us initial states with these already in place.
90+
def _remove_cell_map_definedness(_kast):
91+
if type(_kast) is KApply:
92+
if _kast.label.name.endswith('CellMap:in_keys'):
93+
return Bool.false
94+
elif _kast.label.name.endswith('CellMapItem'):
95+
return _kast.args[1]
96+
return _kast
3497

35-
def get_production_for_klabel(definition, klabel):
36-
productions = [prod for prod in get_productions(definition) if prod.klabel and prod.klabel == klabel]
37-
if len(productions) != 1:
38-
raise ValueError(f'Expected 1 production for label {klabel}, not {productions}.')
39-
return productions[0]
98+
new_term = substitute(init_term, free_vars_subst)
99+
new_term = remove_generated_cells(new_term)
100+
new_term = bottom_up(_remove_cell_map_definedness, new_term)
40101

102+
if not (type(new_term) is KApply and new_term.label.name in ['#Top', '#Bottom']):
103+
config, constraint = split_config_and_constraints(new_term)
104+
constraints = [bool_to_ml_pred(ml_pred_to_bool(c, unsafe=True)) for c in flatten_label('#And', constraint)]
105+
new_term = mlAnd([config] + constraints)
106+
new_term = undo_aliases(defn, new_term)
41107

42-
def get_label_for_cell_sorts(definition, sort):
43-
productions = []
44-
for production in get_productions(definition):
45-
if production.sort == sort and len(production.items) >= 2:
46-
first_arg = production.items[0]
47-
if type(first_arg) is KTerminal and not (
48-
first_arg.value.startswith('project:')
49-
or first_arg.value.startswith('init')
50-
or first_arg.value.startswith('get')
51-
):
52-
productions.append(production)
53-
if len(productions) != 1:
54-
raise ValueError(f'Expected 1 production for sort {sort}, not {productions}!')
55-
return productions[0].klabel
108+
return new_term

0 commit comments

Comments
 (0)