Skip to content

Commit cf89976

Browse files
ehildenbrv-auditor
andauthored
APRProof specific proof state visualization (runtimeverification/pyk#489)
Fixes: #413 This PR introduces proof-specific viewers, and does a few refactors to make that possible: - Helpers are added to `KCFG` for turning into modules, including: - `KCFG.Split.covers` which returns each individual split as a reverse cover. - `KCFG.NDBranch.edges`, which returns each non-deterministic step as an edge. - `KCFG.Edge.to_rule`, which converts an edge into a K rule (or claim). - Small refactorings to `KCFGShow` structure: - `KCFGShow.dot` returns a `Digraph` directly (for further manipulation) instead of a list of `str`. - `KCFGShow.to_module` which returns a `KFlatModule` is factored out and used directly in `KCFGShow.show`. - Helper methods embedded in other methods in `KCFGShow` are factored out directly to staticmethods of `KCFGShow` so they can be reused easily. - We just treat `stuck` as a node attribute now, printed at the beginning of a node, instead of printing it a second time following a node too. - The way that `KCFGShow` calculates node attributes is adjusted and factor out. - A new class `NodePrinter` is factored out, which by default just uses the attributes the `KCFG` knows about for printing out nodes. - That class is extended by `APRProofNodePrinter` and `APRBMCProofNodePrinter`, which add the additional information about the nodes `pending,terminal,bounded` state. If a node is `terminal` or `bounded`, then the `stuck` attribute is no longer printed for less confusion. - `KCFGShow` now takes an optional `*NodePrinter` as an argument (or builds on itself), so instantiating that class is how you can customize how nodes are displayed in text. - The tests are adapted to use the more specific `*NodePrinter` classes, and the output checked that the more specific node information is indeed being included. - Adds helper predicates `APRProof.is_{pending,terminal}` and `APRProof.is_bounded`, which should behave better in case there are qualified node ids passed in. --------- Co-authored-by: devops <[email protected]>
1 parent da52f6d commit cf89976

File tree

11 files changed

+209
-159
lines changed

11 files changed

+209
-159
lines changed

pyk/package/version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.334
1+
0.1.335

pyk/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "pyk"
7-
version = "0.1.334"
7+
version = "0.1.335"
88
description = ""
99
authors = [
1010
"Runtime Verification, Inc. <[email protected]>",

pyk/src/pyk/kcfg/kcfg.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
from threading import RLock
88
from typing import TYPE_CHECKING, List, Union, cast, final
99

10-
from ..cterm import CSubst, CTerm
10+
from ..cterm import CSubst, CTerm, build_claim, build_rule
11+
from ..kast.inner import KApply
1112
from ..kast.manip import (
1213
bool_to_ml_pred,
1314
extract_lhs,
@@ -24,7 +25,7 @@
2425
from typing import Any
2526

2627
from ..kast.inner import KInner
27-
from ..kast.outer import KClaim, KDefinition
28+
from ..kast.outer import KClaim, KDefinition, KRuleLike
2829

2930

3031
NodeIdLike = int | str
@@ -79,6 +80,22 @@ def to_dict(self) -> dict[str, Any]:
7980
'depth': self.depth,
8081
}
8182

83+
def to_rule(self, label: str, claim: bool = False, priority: int | None = None) -> KRuleLike:
84+
def is_ceil_condition(kast: KInner) -> bool:
85+
return type(kast) is KApply and kast.label.name == '#Ceil'
86+
87+
sentence_id = f'{label}-{self.source.id}-TO-{self.target.id}'
88+
init_constraints = [c for c in self.source.cterm.constraints if not is_ceil_condition(c)]
89+
init_cterm = CTerm(self.source.cterm.config, init_constraints)
90+
target_constraints = [c for c in self.target.cterm.constraints if not is_ceil_condition(c)]
91+
target_cterm = CTerm(self.target.cterm.config, target_constraints)
92+
rule: KRuleLike
93+
if claim:
94+
rule, _ = build_claim(sentence_id, init_cterm, target_cterm)
95+
else:
96+
rule, _ = build_rule(sentence_id, init_cterm, target_cterm, priority=priority)
97+
return rule
98+
8299
@final
83100
@dataclass(frozen=True)
84101
class Cover(EdgeLike):
@@ -139,6 +156,10 @@ def to_dict(self) -> dict[str, Any]:
139156
def with_single_target(self, target: KCFG.Node) -> KCFG.Split:
140157
return KCFG.Split(self.source, ((target, self.splits[target.id]),))
141158

159+
@property
160+
def covers(self) -> tuple[KCFG.Cover, ...]:
161+
return tuple(KCFG.Cover(target, self.source, csubst) for target, csubst in self._targets)
162+
142163
@final
143164
@dataclass(frozen=True)
144165
class NDBranch(MultiEdge):
@@ -162,6 +183,10 @@ def to_dict(self) -> dict[str, Any]:
162183
def with_single_target(self, target: KCFG.Node) -> KCFG.NDBranch:
163184
return KCFG.NDBranch(self.source, (target,))
164185

186+
@property
187+
def edges(self) -> tuple[KCFG.Edge, ...]:
188+
return tuple(KCFG.Edge(self.source, target, 1) for target in self.targets)
189+
165190
_node_id: int
166191
_nodes: dict[int, Node]
167192
_edges: dict[int, dict[int, Edge]]

pyk/src/pyk/kcfg/show.py

Lines changed: 90 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from graphviz import Digraph
77

8-
from ..cterm import CTerm, build_claim, build_rule
98
from ..kast.inner import KApply, KRewrite, top_down
109
from ..kast.manip import (
1110
flatten_label,
@@ -22,29 +21,40 @@
2221
from .kcfg import KCFG
2322

2423
if TYPE_CHECKING:
25-
from collections.abc import Callable, Iterable
24+
from collections.abc import Iterable
2625
from pathlib import Path
2726
from typing import Final
2827

2928
from ..kast import KInner
30-
from ..kast.outer import KRuleLike
29+
from ..kast.outer import KDefinition
3130
from ..ktool.kprint import KPrint
3231
from .kcfg import NodeIdLike
3332

3433
_LOGGER: Final = logging.getLogger(__name__)
3534

3635

37-
class KCFGShow:
36+
class NodePrinter:
3837
kprint: KPrint
38+
full_printer: bool
39+
minimize: bool
3940

40-
def __init__(
41-
self,
42-
kprint: KPrint,
43-
):
41+
def __init__(self, kprint: KPrint, full_printer: bool = False, minimize: bool = False):
4442
self.kprint = kprint
43+
self.full_printer = full_printer
44+
self.minimize = minimize
45+
46+
def print_node(self, kcfg: KCFG, node: KCFG.Node) -> list[str]:
47+
attrs = self.node_attrs(kcfg, node)
48+
attr_str = ' (' + ', '.join(attrs) + ')' if attrs else ''
49+
node_strs = [f'{node.id}{attr_str}']
50+
if self.full_printer:
51+
kast = node.cterm.kast
52+
if self.minimize:
53+
kast = minimize_term(kast)
54+
node_strs.extend(' ' + line for line in self.kprint.pretty_print(kast).split('\n'))
55+
return node_strs
4556

46-
@staticmethod
47-
def node_attrs(kcfg: KCFG, node: KCFG.Node) -> list[str]:
57+
def node_attrs(self, kcfg: KCFG, node: KCFG.Node) -> list[str]:
4858
attrs = []
4959
if kcfg.is_init(node.id):
5060
attrs.append('init')
@@ -60,25 +70,43 @@ def node_attrs(kcfg: KCFG, node: KCFG.Node) -> list[str]:
6070
attrs.append('leaf')
6171
if kcfg.is_split(node.id):
6272
attrs.append('split')
73+
attrs.extend(['@' + alias for alias in sorted(kcfg.aliases(node.id))])
6374
return attrs
6475

76+
77+
class KCFGShow:
78+
kprint: KPrint
79+
node_printer: NodePrinter
80+
81+
def __init__(self, kprint: KPrint, node_printer: NodePrinter | None = None):
82+
self.kprint = kprint
83+
self.node_printer = node_printer if node_printer is not None else NodePrinter(kprint)
84+
85+
def node_short_info(self, kcfg: KCFG, node: KCFG.Node) -> list[str]:
86+
return self.node_printer.print_node(kcfg, node)
87+
6588
@staticmethod
66-
def node_short_info(
67-
kcfg: KCFG, node: KCFG.Node, node_printer: Callable[[CTerm], Iterable[str]] | None = None
68-
) -> list[str]:
69-
attrs = KCFGShow.node_attrs(kcfg, node) + ['@' + alias for alias in sorted(kcfg.aliases(node.id))]
70-
attr_string = ' (' + ', '.join(attrs) + ')' if attrs else ''
71-
node_header = str(node.id) + attr_string
72-
node_strs = [node_header]
73-
if node_printer:
74-
node_strs.extend(f' {nl}' for nl in node_printer(node.cterm))
75-
return node_strs
89+
def hide_cells(term: KInner, omit_cells: Iterable[str]) -> KInner:
90+
def _hide_cells(_k: KInner) -> KInner:
91+
if type(_k) == KApply and _k.label.name in omit_cells:
92+
return DOTS
93+
return _k
94+
95+
if omit_cells:
96+
return top_down(_hide_cells, term)
97+
return term
98+
99+
@staticmethod
100+
def simplify_config(defn: KDefinition, config: KInner, omit_cells: Iterable[str]) -> KInner:
101+
config = inline_cell_maps(config)
102+
config = sort_ac_collections(defn, config)
103+
config = KCFGShow.hide_cells(config, omit_cells)
104+
return config
76105

77106
def pretty_segments(
78107
self,
79108
kcfg: KCFG,
80109
minimize: bool = True,
81-
node_printer: Callable[[CTerm], Iterable[str]] | None = None,
82110
) -> Iterable[tuple[str, Iterable[str]]]:
83111
"""Return a pretty version of the KCFG in segments.
84112
@@ -90,17 +118,8 @@ def pretty_segments(
90118
processed_nodes: list[KCFG.Node] = []
91119
ret_lines: list[tuple[str, list[str]]] = []
92120

93-
def _bold(text: str) -> str:
94-
return '\033[1m' + text + '\033[0m'
95-
96-
def _green(text: str) -> str:
97-
return '\033[32m' + text + '\033[0m'
98-
99121
def _print_node(node: KCFG.Node) -> list[str]:
100-
short_info = KCFGShow.node_short_info(kcfg, node, node_printer=node_printer)
101-
if kcfg.is_frontier(node.id):
102-
short_info[0] = _bold(short_info[0])
103-
return short_info
122+
return self.node_short_info(kcfg, node)
104123

105124
def _print_edge(edge: KCFG.Edge) -> list[str]:
106125
if edge.depth == 1:
@@ -170,8 +189,6 @@ def _print_subgraph(indent: str, curr_node: KCFG.Node, prior_on_trace: list[KCFG
170189
suffix = ['(looped back)', '']
171190
elif processed and not kcfg.is_target(curr_node.id):
172191
suffix = ['(continues as previously)', '']
173-
elif kcfg.is_stuck(curr_node.id):
174-
suffix = ['(stuck)', '']
175192
else:
176193
suffix = ['']
177194
ret_node_lines.append(indent + elbow + ' ' + curr_node_strs[0])
@@ -286,17 +303,8 @@ def pretty(
286303
self,
287304
kcfg: KCFG,
288305
minimize: bool = True,
289-
node_printer: Callable[[CTerm], Iterable[str]] | None = None,
290306
) -> Iterable[str]:
291-
return (
292-
line
293-
for _, seg_lines in self.pretty_segments(
294-
kcfg,
295-
minimize=minimize,
296-
node_printer=node_printer,
297-
)
298-
for line in seg_lines
299-
)
307+
return (line for _, seg_lines in self.pretty_segments(kcfg, minimize=minimize) for line in seg_lines)
300308

301309
def show(
302310
self,
@@ -307,37 +315,17 @@ def show(
307315
to_module: bool = False,
308316
minimize: bool = True,
309317
sort_collections: bool = False,
310-
node_printer: Callable[[CTerm], Iterable[str]] | None = None,
311318
omit_cells: Iterable[str] = (),
312319
) -> list[str]:
313320
res_lines: list[str] = []
314-
res_lines += self.pretty(cfg, minimize=minimize, node_printer=node_printer)
315-
316-
def hide_cells(term: KInner) -> KInner:
317-
def _hide_cells(_k: KInner) -> KInner:
318-
if type(_k) == KApply and _k.label.name in omit_cells:
319-
return DOTS
320-
return _k
321-
322-
if omit_cells:
323-
return top_down(_hide_cells, term)
324-
return term
325-
326-
def simplify_config(config: KInner) -> KInner:
327-
config = inline_cell_maps(config)
328-
config = sort_ac_collections(self.kprint.definition, config)
329-
config = hide_cells(config)
330-
return config
331-
332-
def is_ceil_condition(kast: KInner) -> bool:
333-
return type(kast) is KApply and kast.label.name == '#Ceil'
321+
res_lines += self.pretty(cfg, minimize=minimize)
334322

335323
nodes_printed = False
336324

337325
for node_id in nodes:
338326
nodes_printed = True
339327
kast = cfg.node(node_id).cterm.kast
340-
kast = hide_cells(kast)
328+
kast = KCFGShow.hide_cells(kast, omit_cells)
341329
if minimize:
342330
kast = minimize_term(kast)
343331
res_lines.append('')
@@ -349,8 +337,8 @@ def is_ceil_condition(kast: KInner) -> bool:
349337

350338
for node_id_1, node_id_2 in node_deltas:
351339
nodes_printed = True
352-
config_1 = simplify_config(cfg.node(node_id_1).cterm.config)
353-
config_2 = simplify_config(cfg.node(node_id_2).cterm.config)
340+
config_1 = KCFGShow.simplify_config(self.kprint.definition, cfg.node(node_id_1).cterm.config, omit_cells)
341+
config_2 = KCFGShow.simplify_config(self.kprint.definition, cfg.node(node_id_2).cterm.config, omit_cells)
354342
config_delta = push_down_rewrites(KRewrite(config_1, config_2))
355343
if minimize:
356344
config_delta = minimize_term(config_delta)
@@ -367,35 +355,39 @@ def is_ceil_condition(kast: KInner) -> bool:
367355
res_lines.append('')
368356

369357
if to_module:
370-
371-
def to_rule(edge: KCFG.Edge, *, claim: bool = False) -> KRuleLike:
372-
sentence_id = f'BASIC-BLOCK-{edge.source.id}-TO-{edge.target.id}'
373-
init_constraints = [c for c in edge.source.cterm.constraints if not is_ceil_condition(c)]
374-
init_cterm = CTerm(simplify_config(edge.source.cterm.config), init_constraints)
375-
target_constraints = [c for c in edge.target.cterm.constraints if not is_ceil_condition(c)]
376-
target_cterm = CTerm(simplify_config(edge.target.cterm.config), target_constraints)
377-
rule: KRuleLike
378-
if claim:
379-
rule, _ = build_claim(sentence_id, init_cterm, target_cterm)
380-
else:
381-
rule, _ = build_rule(sentence_id, init_cterm, target_cterm, priority=35)
382-
return rule
383-
384-
rules = [to_rule(e) for e in cfg.edges()]
385-
nd_steps = [
386-
to_rule(KCFG.Edge(ndbranch.source, target, 1))
387-
for ndbranch in cfg.ndbranches()
388-
for target in ndbranch.targets
389-
]
390-
claims = [to_rule(KCFG.Edge(nd, cfg.get_unique_target(), -1), claim=True) for nd in cfg.frontier]
391-
cfg_module_name = cfgid.upper().replace('.', '-').replace('_', '-')
392-
new_module = KFlatModule(f'SUMMARY-{cfg_module_name}', rules + nd_steps + claims)
393-
res_lines.append(self.kprint.pretty_print(new_module, sort_collections=sort_collections))
394-
res_lines.append('')
358+
module = self.to_module(cfgid, cfg, omit_cells=omit_cells)
359+
res_lines.append(self.kprint.pretty_print(module, sort_collections=sort_collections))
395360

396361
return res_lines
397362

398-
def dot(self, kcfg: KCFG, node_printer: Callable[[CTerm], Iterable[str]] | None = None) -> str:
363+
def to_module(
364+
self,
365+
cfgid: str,
366+
cfg: KCFG,
367+
module_name: str | None = None,
368+
imports: Iterable[str] = (),
369+
omit_cells: Iterable[str] = (),
370+
) -> KFlatModule:
371+
rules = [e.to_rule('BASIC-BLOCK') for e in cfg.edges()]
372+
rules = [
373+
r.let(body=KCFGShow.simplify_config(self.kprint.definition, r.body, omit_cells=omit_cells)) for r in rules
374+
]
375+
nd_steps = [edge.to_rule('ND-STEP') for ndbranch in cfg.ndbranches() for edge in ndbranch.edges]
376+
nd_steps = [
377+
r.let(body=KCFGShow.simplify_config(self.kprint.definition, r.body, omit_cells=omit_cells))
378+
for r in nd_steps
379+
]
380+
claims = [KCFG.Edge(nd, cfg.get_unique_target(), -1).to_rule('UNPROVEN', claim=True) for nd in cfg.frontier]
381+
claims = [
382+
c.let(body=KCFGShow.simplify_config(self.kprint.definition, c.body, omit_cells=omit_cells)) for c in claims
383+
]
384+
385+
cfg_module_name = (
386+
module_name if module_name is not None else f'SUMMARY-{cfgid.upper().replace(".", "-").replace("_", "-")}'
387+
)
388+
return KFlatModule(cfg_module_name, rules + nd_steps + claims)
389+
390+
def dot(self, kcfg: KCFG) -> Digraph:
399391
def _short_label(label: str) -> str:
400392
return '\n'.join(
401393
[
@@ -407,8 +399,8 @@ def _short_label(label: str) -> str:
407399
graph = Digraph()
408400

409401
for node in kcfg.nodes:
410-
label = '\n'.join(KCFGShow.node_short_info(kcfg, node, node_printer=node_printer))
411-
class_attrs = ' '.join(KCFGShow.node_attrs(kcfg, node))
402+
label = '\n'.join(self.node_short_info(kcfg, node))
403+
class_attrs = ' '.join(self.node_printer.node_attrs(kcfg, node))
412404
attrs = {'class': class_attrs} if class_attrs else {}
413405
graph.node(name=node.id, label=label, **attrs)
414406

@@ -445,24 +437,17 @@ def _short_label(label: str) -> str:
445437
attrs = {'class': 'target', 'style': 'solid'}
446438
graph.edge(tail_name=node.id, head_name=target_id, label=' false', **attrs)
447439

448-
return graph.source
440+
return graph
449441

450-
def dump(
451-
self,
452-
cfgid: str,
453-
cfg: KCFG,
454-
dump_dir: Path,
455-
dot: bool = False,
456-
node_printer: Callable[[CTerm], Iterable[str]] | None = None,
457-
) -> None:
442+
def dump(self, cfgid: str, cfg: KCFG, dump_dir: Path, dot: bool = False) -> None:
458443
ensure_dir_path(dump_dir)
459444

460445
cfg_file = dump_dir / f'{cfgid}.json'
461446
cfg_file.write_text(cfg.to_json())
462447
_LOGGER.info(f'Wrote CFG file {cfgid}: {cfg_file}')
463448

464449
if dot:
465-
cfg_dot = self.dot(cfg, node_printer=node_printer)
450+
cfg_dot = self.dot(cfg)
466451
dot_file = dump_dir / f'{cfgid}.dot'
467452
dot_file.write_text(cfg_dot)
468453
_LOGGER.info(f'Wrote DOT file {cfgid}: {dot_file}')

0 commit comments

Comments
 (0)