Skip to content

Commit 98f057e

Browse files
authored
MergedEdge class for merging node functionality (#4603)
Here is the alternative implementation for [this PR](#4600). - commit 1: introduce the new class `MergedEdge` - commit 2: add `to_rule` function to `MergedEdge` for CSE - commit 3: add corresponding functions like `Edge` for `MergedEdge` - commit 4: modify `Edge.to_rule` related functions for CSE - commit 5: modify KCFGShow for MergedEdge - commit 6: modify KCFGViewer for MergedEdge
1 parent 1e0db61 commit 98f057e

File tree

5 files changed

+174
-22
lines changed

5 files changed

+174
-22
lines changed

pyk/src/pyk/kcfg/kcfg.py

Lines changed: 109 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,41 @@ def replace_target(self, node: KCFG.Node) -> KCFG.Edge:
245245
assert node.id == self.target.id
246246
return KCFG.Edge(self.source, node, self.depth, self.rules)
247247

248+
@final
249+
@dataclass(frozen=True)
250+
class MergedEdge(EdgeLike):
251+
"""Merged edge is a collection of edges that have been merged into a single edge."""
252+
253+
source: KCFG.Node
254+
target: KCFG.Node
255+
edges: tuple[KCFG.Edge, ...]
256+
257+
def to_dict(self) -> dict[str, Any]:
258+
return {
259+
'source': self.source.id,
260+
'target': self.target.id,
261+
'edges': [edge.to_dict() for edge in self.edges],
262+
}
263+
264+
@staticmethod
265+
def from_dict(dct: dict[str, Any], nodes: Mapping[int, KCFG.Node]) -> KCFG.Successor:
266+
return KCFG.MergedEdge(
267+
nodes[dct['source']],
268+
nodes[dct['target']],
269+
tuple(KCFG.Edge.from_dict(edge, nodes) for edge in dct['edges']),
270+
)
271+
272+
def replace_source(self, node: KCFG.Node) -> KCFG.Successor:
273+
assert node.id == self.source.id
274+
return KCFG.MergedEdge(node, self.target, self.edges)
275+
276+
def replace_target(self, node: KCFG.Node) -> KCFG.Successor:
277+
assert node.id == self.target.id
278+
return KCFG.MergedEdge(self.source, node, self.edges)
279+
280+
def to_rule(self, label: str, claim: bool = False, priority: int | None = None) -> KRuleLike:
281+
return KCFG.Edge(self.source, self.target, 1, ()).to_rule(label, claim, priority)
282+
248283
@final
249284
@dataclass(frozen=True)
250285
class Cover(EdgeLike):
@@ -389,6 +424,7 @@ def replace_target(self, node: KCFG.Node) -> KCFG.NDBranch:
389424
_deleted_nodes: set[int]
390425

391426
_edges: dict[int, Edge]
427+
_merged_edges: dict[int, MergedEdge]
392428
_covers: dict[int, Cover]
393429
_splits: dict[int, Split]
394430
_ndbranches: dict[int, NDBranch]
@@ -408,6 +444,7 @@ def __init__(self, cfg_dir: Path | None = None, optimize_memory: bool = True) ->
408444
self._created_nodes = set()
409445
self._deleted_nodes = set()
410446
self._edges = {}
447+
self._merged_edges = {}
411448
self._covers = {}
412449
self._splits = {}
413450
self._ndbranches = {}
@@ -421,6 +458,8 @@ def __contains__(self, item: object) -> bool:
421458
return self.contains_node(item)
422459
if type(item) is KCFG.Edge:
423460
return self.contains_edge(item)
461+
if type(item) is KCFG.MergedEdge:
462+
return self.contains_merged_edge(item)
424463
if type(item) is KCFG.Cover:
425464
return self.contains_cover(item)
426465
if type(item) is KCFG.Split:
@@ -502,6 +541,8 @@ def path_length(_path: Iterable[KCFG.Successor]) -> int:
502541
return 1 + KCFG.path_length(_path[1:])
503542
elif type(_path[0]) is KCFG.Edge:
504543
return _path[0].depth + KCFG.path_length(_path[1:])
544+
elif type(_path[0]) is KCFG.MergedEdge:
545+
return min(edge.depth for edge in _path[0].edges) + KCFG.path_length(_path[1:]) # todo: check this
505546
raise ValueError(f'Cannot handle Successor type: {type(_path[0])}')
506547

507548
def extend(
@@ -576,6 +617,7 @@ def to_dict_no_nodes(self) -> dict[str, Any]:
576617
def to_dict(self) -> dict[str, Any]:
577618
nodes = [node.to_dict() for node in self.nodes]
578619
edges = [edge.to_dict() for edge in self.edges()]
620+
merged_edges = [merged_edge.to_dict() for merged_edge in self.merged_edges()]
579621
covers = [cover.to_dict() for cover in self.covers()]
580622
splits = [split.to_dict() for split in self.splits()]
581623
ndbranches = [ndbranch.to_dict() for ndbranch in self.ndbranches()]
@@ -586,6 +628,7 @@ def to_dict(self) -> dict[str, Any]:
586628
'next': self._node_id,
587629
'nodes': nodes,
588630
'edges': edges,
631+
'merged_edges': merged_edges,
589632
'covers': covers,
590633
'splits': splits,
591634
'ndbranches': ndbranches,
@@ -608,6 +651,10 @@ def from_dict(dct: Mapping[str, Any], optimize_memory: bool = True) -> KCFG:
608651
edge = KCFG.Edge.from_dict(edge_dict, cfg._nodes)
609652
cfg.add_successor(edge)
610653

654+
for edge_dict in dct.get('merged_edges') or []:
655+
merged_edge = KCFG.MergedEdge.from_dict(edge_dict, cfg._nodes)
656+
cfg.add_successor(merged_edge)
657+
611658
for cover_dict in dct.get('covers') or []:
612659
cover = KCFG.Cover.from_dict(cover_dict, cfg._nodes)
613660
cfg.add_successor(cover)
@@ -636,9 +683,11 @@ def to_json(self) -> str:
636683
def from_json(s: str, optimize_memory: bool = True) -> KCFG:
637684
return KCFG.from_dict(json.loads(s), optimize_memory=optimize_memory)
638685

639-
def to_rules(self, priority: int = 20, id: str | None = None) -> list[KRuleLike]:
640-
id = '' if id is None else f'{id}-'
641-
return [e.to_rule(f'{id}BASIC-BLOCK', priority=priority) for e in self.edges()]
686+
def to_rules(self, _id: str | None = None, priority: int = 20) -> list[KRuleLike]:
687+
_id = 'BASIC-BLOCK' if _id is None else _id
688+
return [e.to_rule(_id, priority=priority) for e in self.edges()] + [
689+
m.to_rule(_id, priority=priority) for m in self.merged_edges()
690+
]
642691

643692
def to_module(
644693
self,
@@ -707,6 +756,9 @@ def remove_node(self, node_id: NodeIdLike) -> None:
707756
self._deleted_nodes.add(node.id)
708757

709758
self._edges = {k: s for k, s in self._edges.items() if k != node_id and node_id not in s.target_ids}
759+
self._merged_edges = {
760+
k: s for k, s in self._merged_edges.items() if k != node_id and node_id not in s.target_ids
761+
}
710762
self._covers = {k: s for k, s in self._covers.items() if k != node_id and node_id not in s.target_ids}
711763

712764
self._splits = {k: s for k, s in self._splits.items() if k != node_id and node_id not in s.target_ids}
@@ -721,6 +773,8 @@ def _update_refs(self, node_id: int) -> None:
721773
new_succ = succ.replace_source(node)
722774
if type(new_succ) is KCFG.Edge:
723775
self._edges[new_succ.source.id] = new_succ
776+
if type(new_succ) is KCFG.MergedEdge:
777+
self._merged_edges[new_succ.source.id] = new_succ
724778
if type(new_succ) is KCFG.Cover:
725779
self._covers[new_succ.source.id] = new_succ
726780
if type(new_succ) is KCFG.Split:
@@ -732,6 +786,8 @@ def _update_refs(self, node_id: int) -> None:
732786
new_pred = pred.replace_target(node)
733787
if type(new_pred) is KCFG.Edge:
734788
self._edges[new_pred.source.id] = new_pred
789+
if type(new_pred) is KCFG.MergedEdge:
790+
self._merged_edges[new_pred.source.id] = new_pred
735791
if type(new_pred) is KCFG.Cover:
736792
self._covers[new_pred.source.id] = new_pred
737793
if type(new_pred) is KCFG.Split:
@@ -768,17 +824,19 @@ def replace_node(self, node: KCFG.Node) -> None:
768824

769825
def successors(self, source_id: NodeIdLike) -> list[Successor]:
770826
out_edges: Iterable[KCFG.Successor] = self.edges(source_id=source_id)
827+
out_merged_edges: Iterable[KCFG.Successor] = self.merged_edges(source_id=source_id)
771828
out_covers: Iterable[KCFG.Successor] = self.covers(source_id=source_id)
772829
out_splits: Iterable[KCFG.Successor] = self.splits(source_id=source_id)
773830
out_ndbranches: Iterable[KCFG.Successor] = self.ndbranches(source_id=source_id)
774-
return list(out_edges) + list(out_covers) + list(out_splits) + list(out_ndbranches)
831+
return list(out_edges) + list(out_merged_edges) + list(out_covers) + list(out_splits) + list(out_ndbranches)
775832

776833
def predecessors(self, target_id: NodeIdLike) -> list[Successor]:
777834
in_edges: Iterable[KCFG.Successor] = self.edges(target_id=target_id)
835+
in_merged_edges: Iterable[KCFG.Successor] = self.merged_edges(target_id=target_id)
778836
in_covers: Iterable[KCFG.Successor] = self.covers(target_id=target_id)
779837
in_splits: Iterable[KCFG.Successor] = self.splits(target_id=target_id)
780838
in_ndbranches: Iterable[KCFG.Successor] = self.ndbranches(target_id=target_id)
781-
return list(in_edges) + list(in_covers) + list(in_splits) + list(in_ndbranches)
839+
return list(in_edges) + list(in_merged_edges) + list(in_covers) + list(in_splits) + list(in_ndbranches)
782840

783841
def _check_no_successors(self, source_id: NodeIdLike) -> None:
784842
if len(self.successors(source_id)) > 0:
@@ -797,6 +855,8 @@ def add_successor(self, succ: KCFG.Successor) -> None:
797855
self._check_no_zero_loops(succ.source.id, succ.target_ids)
798856
if type(succ) is KCFG.Edge:
799857
self._edges[succ.source.id] = succ
858+
elif type(succ) is KCFG.MergedEdge:
859+
self._merged_edges[succ.source.id] = succ
800860
elif type(succ) is KCFG.Cover:
801861
self._covers[succ.source.id] = succ
802862
else:
@@ -846,6 +906,46 @@ def remove_edge(self, source_id: NodeIdLike, target_id: NodeIdLike) -> None:
846906
raise ValueError(f'Edge does not exist: {source_id} -> {target_id}')
847907
self._edges.pop(source_id)
848908

909+
def merged_edge(self, source_id: NodeIdLike, target_id: NodeIdLike) -> MergedEdge | None:
910+
source_id = self._resolve(source_id)
911+
target_id = self._resolve(target_id)
912+
merged_edge = self._merged_edges.get(source_id, None)
913+
return merged_edge if merged_edge is not None and merged_edge.target.id == target_id else None
914+
915+
def merged_edges(
916+
self, *, source_id: NodeIdLike | None = None, target_id: NodeIdLike | None = None
917+
) -> list[MergedEdge]:
918+
source_id = self._resolve(source_id) if source_id is not None else None
919+
target_id = self._resolve(target_id) if target_id is not None else None
920+
return [
921+
merged_edge
922+
for merged_edge in self._merged_edges.values()
923+
if (source_id is None or source_id == merged_edge.source.id)
924+
and (target_id is None or target_id == merged_edge.target.id)
925+
]
926+
927+
def contains_merged_edge(self, edge: MergedEdge) -> bool:
928+
if other := self.merged_edge(source_id=edge.source.id, target_id=edge.target.id):
929+
return edge == other
930+
return False
931+
932+
def create_merged_edge(self, source_id: NodeIdLike, target_id: NodeIdLike, edges: Iterable[Edge]) -> MergedEdge:
933+
if len(list(edges)) == 0:
934+
raise ValueError(f'Cannot build KCFG MergedEdge with no edges: {edges}')
935+
source = self.node(source_id)
936+
target = self.node(target_id)
937+
merged_edge = KCFG.MergedEdge(source, target, tuple(edges))
938+
self.add_successor(merged_edge)
939+
return merged_edge
940+
941+
def remove_merged_edge(self, source_id: NodeIdLike, target_id: NodeIdLike) -> None:
942+
source_id = self._resolve(source_id)
943+
target_id = self._resolve(target_id)
944+
merged_edge = self.merged_edge(source_id, target_id)
945+
if not merged_edge:
946+
raise ValueError(f'MergedEdge does not exist: {source_id} -> {target_id}')
947+
self._merged_edges.pop(source_id)
948+
849949
def cover(self, source_id: NodeIdLike, target_id: NodeIdLike) -> Cover | None:
850950
source_id = self._resolve(source_id)
851951
target_id = self._resolve(target_id)
@@ -887,8 +987,10 @@ def remove_cover(self, source_id: NodeIdLike, target_id: NodeIdLike) -> None:
887987
self._covers.pop(source_id)
888988

889989
def edge_likes(self, *, source_id: NodeIdLike | None = None, target_id: NodeIdLike | None = None) -> list[EdgeLike]:
890-
return cast('List[KCFG.EdgeLike]', self.edges(source_id=source_id, target_id=target_id)) + cast(
891-
'List[KCFG.EdgeLike]', self.covers(source_id=source_id, target_id=target_id)
990+
return (
991+
cast('List[KCFG.EdgeLike]', self.edges(source_id=source_id, target_id=target_id))
992+
+ cast('List[KCFG.EdgeLike]', self.covers(source_id=source_id, target_id=target_id))
993+
+ cast('List[KCFG.EdgeLike]', self.merged_edges(source_id=source_id, target_id=target_id))
892994
)
893995

894996
def add_vacuous(self, node_id: NodeIdLike) -> None:

pyk/src/pyk/kcfg/show.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ def _print_edge(edge: KCFG.Edge) -> list[str]:
135135
else:
136136
return ['(' + str(edge.depth) + ' steps)']
137137

138+
def _print_merged_edge(merged_edge: KCFG.MergedEdge) -> list[str]:
139+
res = '('
140+
for edge in merged_edge.edges:
141+
res += f'{edge.depth}|'
142+
res = res[:-1] + ' steps)'
143+
return [res] if len(res) < 78 else ['(merged edge)']
144+
138145
def _print_cover(cover: KCFG.Cover) -> Iterable[str]:
139146
subst_strs = [f'{k} <- {self.kprint.pretty_print(v)}' for k, v in cover.csubst.subst.items()]
140147
subst_str = ''
@@ -249,6 +256,11 @@ def _print_subgraph(indent: str, curr_node: KCFG.Node, prior_on_trace: list[KCFG
249256
ret_edge_lines.extend(add_indent(indent + '│ ', _print_edge(successor)))
250257
ret_lines.append((f'edge_{successor.source.id}_{successor.target.id}', ret_edge_lines))
251258

259+
elif type(successor) is KCFG.MergedEdge:
260+
ret_edge_lines = []
261+
ret_edge_lines.extend(add_indent(indent + '│ ', _print_merged_edge(successor)))
262+
ret_lines.append((f'merged_edge_{successor.source.id}_{successor.target.id}', ret_edge_lines))
263+
252264
elif type(successor) is KCFG.Cover:
253265
ret_edge_lines = []
254266
ret_edge_lines.extend(add_indent(indent + '┊ ', _print_cover(successor)))

pyk/src/pyk/kcfg/tui.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,10 @@ def _info_text(self) -> str:
224224
element_str = f'node({shorten_hashes(self._element.id)})'
225225
elif type(self._element) is KCFG.Edge:
226226
element_str = f'edge({shorten_hashes(self._element.source.id)},{shorten_hashes(self._element.target.id)})'
227+
elif type(self._element) is KCFG.MergedEdge:
228+
element_str = (
229+
f'merged_edge({shorten_hashes(self._element.source.id)},{shorten_hashes(self._element.target.id)})'
230+
)
227231
elif type(self._element) is KCFG.Cover:
228232
element_str = f'cover({shorten_hashes(self._element.source.id)},{shorten_hashes(self._element.target.id)})'
229233
return f'{element_str} selected. {minimize_str} Minimize Output. {term_str} Term View. {constraint_str} Constraint View. {status_str} Status View. {custom_str}'
@@ -296,6 +300,14 @@ def _cterm_text(cterm: CTerm) -> tuple[str, str]:
296300
crewrite = CTerm(config, constraints_new)
297301
term_str, constraint_str = _cterm_text(crewrite)
298302

303+
elif type(self._element) is KCFG.MergedEdge:
304+
config_source, *constraints_source = self._element.source.cterm
305+
config_target, *constraints_target = self._element.target.cterm
306+
constraints_new = [c for c in constraints_target if c not in constraints_source]
307+
config = push_down_rewrites(KRewrite(config_source, config_target))
308+
crewrite = CTerm(config, constraints_new)
309+
term_str, constraint_str = _cterm_text(crewrite)
310+
299311
elif type(self._element) is KCFG.Cover:
300312
subst_equalities = map(_boolify, flatten_label('#And', self._element.csubst.subst.ml_pred))
301313
constraints = map(_boolify, flatten_label('#And', self._element.csubst.constraint))
@@ -421,6 +433,14 @@ def on_graph_chunk_selected(self, message: GraphChunk.Selected) -> None:
421433
edge = single(self._kcfg.edges(source_id=source_id, target_id=target_id))
422434
self.query_one('#node-view', NodeView).update(edge)
423435

436+
elif message.chunk_id.startswith('merged_edge_'):
437+
self._selected_chunk = None
438+
node_source, node_target, *_ = message.chunk_id[12:].split('_')
439+
source_id = int(node_source)
440+
target_id = int(node_target)
441+
merged_edge = single(self._kcfg.merged_edges(source_id=source_id, target_id=target_id))
442+
self.query_one('#node-view', NodeView).update(merged_edge)
443+
424444
elif message.chunk_id.startswith('cover_'):
425445
self._selected_chunk = None
426446
node_source, node_target, *_ = message.chunk_id[6:].split('_')

pyk/src/pyk/proof/reachability.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from pathlib import Path
2727
from typing import Any, Final, TypeVar
2828

29-
from ..kast.outer import KClaim, KDefinition, KFlatModuleList
29+
from ..kast.outer import KClaim, KDefinition, KFlatModuleList, KRuleLike
3030
from ..kcfg import KCFGExplore
3131
from ..kcfg.explore import KCFGExtendResult
3232
from ..kcfg.kcfg import CSubst, NodeIdLike
@@ -444,12 +444,12 @@ def as_rules(self, priority: int = 20, direct_rule: bool = False) -> list[KRule]
444444
or (self.admitted and not self.kcfg.predecessors(self.target))
445445
):
446446
return [self.as_rule(priority=priority)]
447-
_rules = []
448-
for _edge in self.kcfg.edges():
449-
_rule = _edge.to_rule(self.rule_id, priority=priority)
450-
assert type(_rule) is KRule
451-
_rules.append(_rule)
452-
return _rules
447+
448+
def _return_rule(r: KRuleLike) -> KRule:
449+
assert isinstance(r, KRule)
450+
return r
451+
452+
return [_return_rule(rule) for rule in self.kcfg.to_rules(self.rule_id, priority=priority)]
453453

454454
def as_rule(self, priority: int = 20) -> KRule:
455455
_edge = KCFG.Edge(self.kcfg.node(self.init), self.kcfg.node(self.target), depth=0, rules=())

0 commit comments

Comments
 (0)