Skip to content

Commit 9abc842

Browse files
ehildenbrv-jenkins
andauthored
CFG visualization and progress saving (#1447)
* kevm-pyk/__main__: _LOGGER.error => ValueError * kevm-pyk/__main__: add foundry-show-cfg endpoint * bin/kevm: expose kevm foundry-show-cfg * kevm-pyk/__main__: add ability to display individual nodes too * kevm-pyk/kevm: bring in KEVM.add_language_invariants * kevm-pyk/solc_to_k: make sure we are starting executions in the language invariant * kevm-pyk/__main__: foundry_out/kcfgs for cfgs, not in kompiled directory * kevm: remove debugging message * kevm-pyk/__main__: add --node-delta option too foundry-show-cfg * kevm-pyk/utils: add KCFG__replace_node routine * kevm-pyk/utils: bring back sanitize_config * kevm-pyk/__main__: option for simplifying initial state at startup * kevm-pyk/__main__: similify target node at startup too * kevm-pyk/__main__: typo * kevm-pyk/__main__: correct how CFG passed/failed logic is handled * bin/kevm, kevm-pyk/__main__: add foundry-list command for seeing KCFG information * bin/kevm, kevm-pyk/__main__: rename show-cfg => show * kevm-pyk/__main__: better list message * kevm-pyk/__main__: remove --store-depth option * kevm-pyk/__main__: factor out _write_cfg method * kevm-pyk/kevm: update KCFG with results of running prover * kevm-pyk/__main__: write cfg to disk * kevm-pyk/__main__: explore from whichever is the fronteir on each call * kevm-pyk/__main__: remove vestigial store_depth * kevm-pyk/__main__: better name for specs * Makefile: dont simplify init/target on CI * kevm-pyk/__main__: more clear about status of proofs * kevm-pyk/__main__: silly logic error * kevm-pyk/__main__: make sure to add expanded attribute to explored nodes * kevm-pyk/__main__: correct variable names * kevm-pyk/__main__: make sure we sanitize configs back from prover Co-authored-by: rv-jenkins <[email protected]>
1 parent d044775 commit 9abc842

File tree

6 files changed

+331
-51
lines changed

6 files changed

+331
-51
lines changed

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,10 @@ tests/foundry/foundry.k.check: tests/foundry/out/kompiled/foundry.k
510510
tests/foundry/out/kompiled/foundry.k: tests/foundry/out/kompiled/timestamp
511511

512512
tests/foundry/out/kompiled/foundry.k.prove: tests/foundry/out/kompiled/timestamp
513-
$(KEVM) foundry-prove tests/foundry/out -j$(FOUNDRY_PAR) $(KEVM_OPTS) $(KPROVE_OPTS) $(addprefix --exclude-test , $(shell cat tests/foundry/exclude))
513+
$(KEVM) foundry-prove tests/foundry/out \
514+
-j$(FOUNDRY_PAR) --no-simplify-init \
515+
$(KEVM_OPTS) $(KPROVE_OPTS) \
516+
$(addprefix --exclude-test , $(shell cat tests/foundry/exclude))
514517

515518
tests/foundry/out/kompiled/timestamp: $(foundry_out) $(KEVM_LIB)/$(foundry_kompiled) venv $(lemma_includes)
516519
$(KEVM) foundry-kompile $< $(KEVM_OPTS) --verbose

bin/kevm

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -287,18 +287,21 @@ run_solc() {
287287
}
288288

289289
run_foundry_kompile() {
290-
local contract_name
291-
292-
echo "${backend_dir}"
293290
run_kevm_pyk foundry-kompile --definition "${backend_dir}" "$@"
294291
}
295292

296293
run_foundry_prove() {
297-
local contract_name
298-
299294
run_kevm_pyk foundry-prove "$@"
300295
}
301296

297+
run_foundry_show() {
298+
run_kevm_pyk foundry-show "$@"
299+
}
300+
301+
run_foundry_list() {
302+
run_kevm_pyk foundry-list "$@"
303+
}
304+
302305
# Main
303306
# ----
304307

@@ -314,8 +317,10 @@ if [[ "$run_command" == 'help' ]] || [[ "$run_command" == '--help' ]] ; then
314317
${KEVM} kompile [--backend (java|llvm|haskell)] [--profile|--debug] <main> <K arg>*
315318
${KEVM} klab-view [--profile|--debug] <spec>
316319
${KEVM} solc-to-k [--profile|--debug] <sol-file> <contract-name> <solc-arg>*
317-
${KEVM} foundry-kompile
318-
${KEVM} foundry-prove
320+
${KEVM} foundry-list # see dedicated help menu
321+
${KEVM} foundry-show # see dedicated help menu
322+
${KEVM} foundry-kompile # see dedicated help menu
323+
${KEVM} foundry-prove # see dedicated help menu
319324
320325
${KEVM} [help|--help|version|--version]
321326
@@ -390,11 +395,13 @@ branching_allowed=
390395
haskell_backend_command=(kore-exec)
391396
kevm_port='8545'
392397
kevm_host='127.0.0.1'
393-
[[ ! "${run_command}" == prove ]] || backend=haskell
394-
[[ ! "${run_command}" == solc-to-k ]] || backend=haskell
395-
[[ ! "${run_command}" == foundry-kompile ]] || backend=foundry
396-
[[ ! "${run_command}" == foundry-prove ]] || backend=foundry
397-
[[ ! "${run_command}" == interpret ]] || unparse=false
398+
[[ ! "${run_command}" == prove ]] || backend=haskell
399+
[[ ! "${run_command}" == solc-to-k ]] || backend=haskell
400+
[[ ! "${run_command}" == foundry-kompile ]] || backend=foundry
401+
[[ ! "${run_command}" == foundry-prove ]] || backend=foundry
402+
[[ ! "${run_command}" == foundry-list ]] || backend=foundry
403+
[[ ! "${run_command}" == foundry-show ]] || backend=foundry
404+
[[ ! "${run_command}" == interpret ]] || unparse=false
398405
args=()
399406
while [[ $# -gt 0 ]]; do
400407
case "$1" in
@@ -438,7 +445,7 @@ if ${pyk}; then
438445
fi
439446

440447
# get the run file
441-
if [[ "${run_command}" != 'foundry-kompile' ]] && [[ "${run_command}" != 'foundry-prove' ]]; then
448+
if [[ "${run_command}" != 'foundry-kompile' ]] && [[ "${run_command}" != 'foundry-prove' ]] && [[ "${run_command}" != 'foundry-show' ]] && [[ "${run_command}" != 'foundry-list' ]]; then
442449
run_file="$1" ; shift
443450
if [[ "${run_file}" == '-' ]]; then
444451
tmp_input="$(mktemp)"
@@ -460,15 +467,17 @@ cCHAINID_kast="#token(\"${chainid}\",\"Int\")"
460467
! ${debug} || set -x
461468

462469
case "$run_command-$backend" in
463-
kompile-@(java|llvm|haskell|node|foundry) ) run_kompile "$@" ;;
464-
run-@(java|llvm|haskell) ) run_krun "$@" ;;
465-
kast-@(java|llvm|haskell) ) run_kast "$@" ;;
466-
interpret-@(llvm|haskell) ) run_interpret "$@" ;;
467-
prove-@(java|haskell|foundry) ) run_prove "$@" ;;
468-
search-@(java|haskell) ) run_search "$@" ;;
469-
klab-view-* ) view_klab "$@" ;;
470-
solc-to-k-* ) run_solc "$@" ;;
471-
foundry-kompile-* ) run_foundry_kompile "$@" ;;
472-
foundry-prove-* ) run_foundry_prove "$@" ;;
470+
kompile-@(java|llvm|haskell|node|foundry) ) run_kompile "$@" ;;
471+
run-@(java|llvm|haskell) ) run_krun "$@" ;;
472+
kast-@(java|llvm|haskell) ) run_kast "$@" ;;
473+
interpret-@(llvm|haskell) ) run_interpret "$@" ;;
474+
prove-@(java|haskell|foundry) ) run_prove "$@" ;;
475+
search-@(java|haskell) ) run_search "$@" ;;
476+
klab-view-* ) view_klab "$@" ;;
477+
solc-to-k-* ) run_solc "$@" ;;
478+
foundry-kompile-* ) run_foundry_kompile "$@" ;;
479+
foundry-prove-* ) run_foundry_prove "$@" ;;
480+
foundry-show-* ) run_foundry_show "$@" ;;
481+
foundry-list-* ) run_foundry_list "$@" ;;
473482
*) ${KEVM} help ; fatal "Unknown command on backend: $run_command $backend" ;;
474483
esac

kevm-pyk/src/kevm_pyk/__main__.py

Lines changed: 180 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@
77

88
from pathos.pools import ProcessPool # type: ignore
99
from pyk.cli_utils import dir_path, file_path
10-
from pyk.kast import KApply, KAtt, KClaim, KDefinition, KFlatModule, KImport, KInner, KRequire, KRule, KToken
11-
from pyk.kastManip import minimize_term
10+
from pyk.cterm import CTerm
11+
from pyk.kast import KApply, KAtt, KDefinition, KFlatModule, KImport, KInner, KRequire, KRewrite, KRule, KToken
12+
from pyk.kastManip import flatten_label, minimize_term, push_down_rewrites
1213
from pyk.kcfg import KCFG
14+
from pyk.ktool.kit import KIT
1315
from pyk.ktool.krun import _krun
14-
from pyk.prelude.ml import mlTop
16+
from pyk.prelude.ml import is_top, mlTop
1517

1618
from .gst_to_kore import gst_to_kore
1719
from .kevm import KEVM, Foundry
1820
from .solc_to_k import Contract, contract_to_main_module, method_to_cfg, solc_compile
19-
from .utils import KPrint_make_unparsing, KProve_prove_claim, add_include_arg
21+
from .utils import KCFG__replace_node, KPrint_make_unparsing, KProve_prove_claim, add_include_arg, sanitize_config
2022

2123
T = TypeVar('T')
2224

@@ -267,19 +269,19 @@ def exec_foundry_prove(
267269
workers: int = 1,
268270
minimize: bool = True,
269271
lemmas: Iterable[str] = (),
272+
simplify_init: bool = True,
270273
**kwargs: Any,
271274
) -> None:
272275
_ignore_arg(kwargs, 'main_module', f'--main-module: {kwargs["main_module"]}')
273276
_ignore_arg(kwargs, 'syntax_module', f'--syntax-module: {kwargs["syntax_module"]}')
274277
_ignore_arg(kwargs, 'definition_dir', f'--definition: {kwargs["definition_dir"]}')
275278
_ignore_arg(kwargs, 'spec_module', f'--spec-module: {kwargs["spec_module"]}')
276279
if workers <= 0:
277-
_LOGGER.error(f'Must have at least one worker, found: --workers {workers}')
278-
sys.exit(1)
280+
raise ValueError(f'Must have at least one worker, found: --workers {workers}')
279281
definition_dir = foundry_out / 'kompiled'
280282
use_directory = foundry_out / 'specs'
281283
use_directory.mkdir(parents=True, exist_ok=True)
282-
kcfgs_dir = definition_dir / 'kcfgs'
284+
kcfgs_dir = foundry_out / 'kcfgs'
283285
if not kcfgs_dir.exists():
284286
kcfgs_dir.mkdir()
285287
foundry = Foundry(definition_dir, profile=profile, use_directory=use_directory)
@@ -315,7 +317,7 @@ def exec_foundry_prove(
315317
if unfound_tests:
316318
raise ValueError(f'Test identifiers not found: {unfound_tests}')
317319

318-
kcfgs: Dict[str, KCFG] = {}
320+
kcfgs: Dict[str, Tuple[KCFG, Path]] = {}
319321
for test in tests:
320322
kcfg_file = kcfgs_dir / f'{test}.json'
321323
if reinit or not kcfg_file.exists():
@@ -325,38 +327,138 @@ def exec_foundry_prove(
325327
method = [m for m in contract.methods if m.name == method_name][0]
326328
empty_config = foundry.definition.empty_config(Foundry.Sorts.FOUNDRY_CELL)
327329
cfg = method_to_cfg(empty_config, contract, method)
328-
kcfgs[test] = cfg
330+
if simplify_init:
331+
_LOGGER.info(f'Simplifying initial state for test: {test}')
332+
edge = KCFG.Edge(cfg.get_unique_init(), cfg.get_unique_target(), mlTop(), -1)
333+
claim = edge.to_claim()
334+
init_simplified = foundry.prove_claim(
335+
claim, f'simplify-init-{cfg.get_unique_init().id}', args=['--depth', '0']
336+
)
337+
init_simplified = sanitize_config(foundry.definition, init_simplified)
338+
cfg = KCFG__replace_node(cfg, cfg.get_unique_init().id, CTerm(init_simplified))
339+
_LOGGER.info(f'Simplifying target state for test: {test}')
340+
edge = KCFG.Edge(cfg.get_unique_target(), cfg.get_unique_init(), mlTop(), -1)
341+
claim = edge.to_claim()
342+
target_simplified = foundry.prove_claim(
343+
claim, f'simplify-target-{cfg.get_unique_target().id}', args=['--depth', '0']
344+
)
345+
target_simplified = sanitize_config(foundry.definition, target_simplified)
346+
cfg = KCFG__replace_node(cfg, cfg.get_unique_target().id, CTerm(target_simplified))
347+
kcfgs[test] = (cfg, kcfg_file)
329348
with open(kcfg_file, 'w') as kf:
330349
kf.write(json.dumps(cfg.to_dict()))
331350
kf.close()
332351
_LOGGER.info(f'Wrote file: {kcfg_file}')
333352
else:
334353
with open(kcfg_file, 'r') as kf:
335-
kcfgs[test] = KCFG.from_dict(json.loads(kf.read()))
336-
337-
def _kcfg_unproven_to_claim(_kcfg: KCFG) -> KClaim:
338-
return _kcfg.create_edge(_kcfg.get_unique_init().id, _kcfg.get_unique_target().id, mlTop(), depth=-1).to_claim()
354+
kcfgs[test] = (KCFG.from_dict(json.loads(kf.read())), kcfg_file)
339355

340356
lemma_rules = [KRule(KToken(lr, 'K'), att=KAtt({'simplification': ''})) for lr in lemmas]
341357

342-
def prove_it(_id_and_cfg: Tuple[str, KCFG]) -> bool:
343-
_cfg_id, _cfg = _id_and_cfg
344-
_claim = _kcfg_unproven_to_claim(_cfg)
358+
def _write_cfg(_cfg: KCFG, _cfgpath: Path) -> None:
359+
with open(_cfgpath, 'w') as cfgfile:
360+
cfgfile.write(json.dumps(_cfg.to_dict()))
361+
_LOGGER.info(f'Updated CFG file: {_cfgpath}')
362+
363+
def prove_it(_id_and_cfg: Tuple[str, Tuple[KCFG, Path]]) -> bool:
364+
_cfg_id, (_cfg, _cfg_path) = _id_and_cfg
365+
if len(_cfg.frontier) == 0:
366+
return True
367+
_init_node = _cfg.frontier[0]
368+
_target_node = _cfg.get_unique_target()
369+
_claim = KCFG.Edge(_init_node, _target_node, mlTop(), -1).to_claim()
345370
_claim_id = _cfg_id.replace('.', '-').replace('_', '-')
346371
ret, result = KProve_prove_claim(foundry, _claim, _claim_id, _LOGGER, depth=depth, lemmas=lemma_rules)
347-
if minimize:
348-
result = minimize_term(result)
349-
print(f'Result for {_cfg_id}:\n{foundry.pretty_print(result)}\n')
350-
return ret
372+
_cfg.add_expanded(_init_node.id)
373+
if is_top(result):
374+
_cfg.create_edge(_cfg.get_unique_init().id, _cfg.get_unique_target().id, mlTop(), -1)
375+
_LOGGER.info(f'Proof passed: {_cfg_id}')
376+
else:
377+
_cfg.add_expanded(_init_node.id)
378+
for result_state in flatten_label('#Or', result):
379+
result_state = sanitize_config(foundry.definition, result_state)
380+
new_node = _cfg.get_or_create_node(CTerm(result_state))
381+
_cfg.create_edge(_cfg.get_unique_init().id, new_node.id, mlTop(), -1)
382+
if minimize:
383+
result_state = minimize_term(result_state)
384+
_LOGGER.error(f'Proof failed: {_cfg_id}\n{foundry.pretty_print(result_state)}')
385+
_write_cfg(_cfg, _cfg_path)
386+
failure_nodes = _cfg.frontier + _cfg.stuck
387+
return len(failure_nodes) == 0
351388

352389
with ProcessPool(ncpus=workers) as process_pool:
353390
results = process_pool.map(prove_it, kcfgs.items())
354391
process_pool.close()
355392

356-
failed_cfgs = [cid for ((cid, _), failed) in zip(kcfgs.items(), results) if failed]
357-
if failed_cfgs:
358-
print(f'Failed to prove KCFGs: {failed_cfgs}\n')
359-
sys.exit(len(failed_cfgs))
393+
failed = 0
394+
for (cid, _), succeeded in zip(kcfgs.items(), results):
395+
if succeeded:
396+
print(f'PASSED: {cid}')
397+
else:
398+
print(f'FAILED: {cid}')
399+
failed += 1
400+
sys.exit(failed)
401+
402+
403+
def exec_foundry_show(
404+
profile: bool,
405+
foundry_out: Path,
406+
test: str,
407+
nodes: Iterable[str] = (),
408+
node_deltas: Iterable[Tuple[str, str]] = (),
409+
minimize: bool = True,
410+
**kwargs: Any,
411+
) -> None:
412+
definition_dir = foundry_out / 'kompiled'
413+
use_directory = foundry_out / 'specs'
414+
use_directory.mkdir(parents=True, exist_ok=True)
415+
kcfgs_dir = foundry_out / 'kcfgs'
416+
foundry = Foundry(definition_dir, profile=profile, use_directory=use_directory)
417+
kcfg_file = kcfgs_dir / f'{test}.json'
418+
with open(kcfg_file, 'r') as kf:
419+
kcfg = KCFG.from_dict(json.loads(kf.read()))
420+
list(map(print, kcfg.pretty(foundry)))
421+
for node_id in nodes:
422+
kast = kcfg.node(node_id).cterm.kast
423+
if minimize:
424+
kast = minimize_term(kast)
425+
print(f'\n\nNode {node_id}:\n\n{foundry.pretty_print(kast)}\n')
426+
for node_id_1, node_id_2 in node_deltas:
427+
config_1 = kcfg.node(node_id_1).cterm.config
428+
config_2 = kcfg.node(node_id_2).cterm.config
429+
config_delta = push_down_rewrites(KRewrite(config_1, config_2))
430+
if minimize:
431+
config_delta = minimize_term(config_delta)
432+
print(f'\n\nState Delta {node_id_1} => {node_id_2}:\n\n{foundry.pretty_print(config_delta)}\n')
433+
434+
435+
def exec_foundry_list(
436+
profile: bool,
437+
foundry_out: Path,
438+
details: bool = True,
439+
**kwargs: Any,
440+
) -> None:
441+
kcfgs_dir = foundry_out / 'kcfgs'
442+
pattern = '*.json'
443+
paths = kcfgs_dir.glob(pattern)
444+
for kcfg_file in paths:
445+
with open(kcfg_file, 'r') as kf:
446+
kcfg = KCFG.from_dict(json.loads(kf.read()))
447+
kcfg_name = kcfg_file.name[0:-5]
448+
total_nodes = len(kcfg.nodes)
449+
frontier_nodes = len(kcfg.frontier)
450+
stuck_nodes = len(kcfg.stuck)
451+
proven = 'failed'
452+
if stuck_nodes == 0:
453+
proven = 'pending'
454+
if frontier_nodes == 0:
455+
proven = 'passed'
456+
print(f'{kcfg_name}: {proven}')
457+
if details:
458+
print(f' nodes: {total_nodes}')
459+
print(f' frontier: {frontier_nodes}')
460+
print(f' stuck: {stuck_nodes}')
461+
print()
360462

361463

362464
def exec_run(
@@ -599,6 +701,60 @@ def parse(s: str) -> List[T]:
599701
action='store_true',
600702
help='Reinitialize KCFGs even if they already exist.',
601703
)
704+
foundry_prove_args.add_argument(
705+
'--simplify-init',
706+
dest='simplify_init',
707+
default=True,
708+
action='store_true',
709+
help='Simplify the initial and target states at startup.',
710+
)
711+
foundry_prove_args.add_argument(
712+
'--no-simplify-init',
713+
dest='simplify_init',
714+
action='store_false',
715+
help='Do not simplify the initial and target states at startup.',
716+
)
717+
718+
foundry_show_args = command_parser.add_parser(
719+
'foundry-show',
720+
help='Display a given Foundry CFG.',
721+
parents=[shared_args, k_args],
722+
)
723+
foundry_show_args.add_argument('foundry_out', type=dir_path, help='Path to Foundry output directory.')
724+
foundry_show_args.add_argument('test', type=str, help='Display the CFG for this test.')
725+
foundry_show_args.add_argument(
726+
'--node',
727+
type=str,
728+
dest='nodes',
729+
default=[],
730+
action='append',
731+
help='List of nodes to display as well.',
732+
)
733+
foundry_show_args.add_argument(
734+
'--node-delta',
735+
type=KIT.arg_pair_of(str, str),
736+
dest='node_deltas',
737+
default=[],
738+
action='append',
739+
help='List of nodes to display delta for.',
740+
)
741+
foundry_show_args.add_argument(
742+
'--minimize', dest='minimize', default=True, action='store_true', help='Minimize output.'
743+
)
744+
foundry_show_args.add_argument(
745+
'--no-minimize', dest='minimize', action='store_false', help='Do not minimize output.'
746+
)
747+
748+
foundry_list_args = command_parser.add_parser(
749+
'foundry-list',
750+
help='List information about KCFGs on disk',
751+
parents=[shared_args, k_args],
752+
)
753+
foundry_list_args.add_argument('foundry_out', type=dir_path, help='Path to Foundry output directory.')
754+
foundry_list_args.add_argument(
755+
'--details', dest='details', default=True, action='store_true', help='Information about progress on each KCFG.'
756+
)
757+
foundry_list_args.add_argument('--no-details', dest='details', action='store_false', help='Just list the KCFGs.')
602758

603759
return parser
604760

0 commit comments

Comments
 (0)