Skip to content

Commit 1e0db61

Browse files
authored
Move kcfg.minimize into a separate file and class (#4607)
Because there will be more pattern rewriting to minimize the KCFG, we need to extract syntax-unrelated functions from `kcfg.py` for clarity. This PR doesn’t introduce any new features; it simply moves functions related to KCFG minimization to `minimize.py`.
1 parent c492344 commit 1e0db61

File tree

5 files changed

+610
-559
lines changed

5 files changed

+610
-559
lines changed

pyk/src/pyk/kcfg/kcfg.py

Lines changed: 6 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@
2323
)
2424
from ..kast.outer import KFlatModule
2525
from ..prelude.kbool import andBool
26-
from ..utils import ensure_dir_path, not_none, single
26+
from ..utils import ensure_dir_path, not_none
27+
from .minimize import KCFGMinimizer
2728

2829
if TYPE_CHECKING:
29-
from collections.abc import Callable, Iterable, Mapping, MutableMapping
30+
from collections.abc import Iterable, Mapping, MutableMapping
3031
from pathlib import Path
3132
from types import TracebackType
3233
from typing import Any
@@ -957,183 +958,6 @@ def split_on_constraints(self, source_id: NodeIdLike, constraints: Iterable[KInn
957958
self.create_split(source.id, zip(branch_node_ids, csubsts, strict=True))
958959
return branch_node_ids
959960

960-
def lift_edge(self, b_id: NodeIdLike) -> None:
961-
"""Lift an edge up another edge directly preceding it.
962-
963-
`A --M steps--> B --N steps--> C` becomes `A --(M + N) steps--> C`. Node `B` is removed.
964-
965-
Args:
966-
b_id: the identifier of the central node `B` of a sequence of edges `A --> B --> C`.
967-
968-
Raises:
969-
AssertionError: If the edges in question are not in place.
970-
"""
971-
# Obtain edges `A -> B`, `B -> C`
972-
a_to_b = single(self.edges(target_id=b_id))
973-
b_to_c = single(self.edges(source_id=b_id))
974-
# Remove the node `B`, effectively removing the entire initial structure
975-
self.remove_node(b_id)
976-
# Create edge `A -> C`
977-
self.create_edge(a_to_b.source.id, b_to_c.target.id, a_to_b.depth + b_to_c.depth, a_to_b.rules + b_to_c.rules)
978-
979-
def lift_edges(self) -> bool:
980-
"""Perform all possible edge lifts across the KCFG.
981-
982-
The KCFG is transformed to an equivalent in which no further edge lifts are possible.
983-
984-
Given the KCFG design, it is not possible for one edge lift to either disallow another or
985-
allow another that was not previously possible. Therefore, this function is guaranteed to
986-
lift all possible edges without having to loop.
987-
988-
Returns:
989-
An indicator of whether or not at least one edge lift was performed.
990-
"""
991-
edges_to_lift = sorted(
992-
[
993-
node.id
994-
for node in self.nodes
995-
if len(self.edges(source_id=node.id)) > 0 and len(self.edges(target_id=node.id)) > 0
996-
]
997-
)
998-
for node_id in edges_to_lift:
999-
self.lift_edge(node_id)
1000-
return len(edges_to_lift) > 0
1001-
1002-
def lift_split_edge(self, b_id: NodeIdLike) -> None:
1003-
"""Lift a split up an edge directly preceding it.
1004-
1005-
`A --M steps--> B --[cond_1, ..., cond_N]--> [C_1, ..., C_N]` becomes
1006-
`A --[cond_1, ..., cond_N]--> [A #And cond_1 --M steps--> C_1, ..., A #And cond_N --M steps--> C_N]`.
1007-
Node `B` is removed.
1008-
1009-
Args:
1010-
b_id: The identifier of the central node `B` of the structure `A --> B --> [C_1, ..., C_N]`.
1011-
1012-
Raises:
1013-
AssertionError: If the structure in question is not in place.
1014-
AssertionError: If any of the `cond_i` contain variables not present in `A`.
1015-
"""
1016-
# Obtain edge `A -> B`, split `[cond_I, C_I | I = 1..N ]`
1017-
a_to_b = single(self.edges(target_id=b_id))
1018-
a = a_to_b.source
1019-
split_from_b = single(self.splits(source_id=b_id))
1020-
ci, csubsts = list(split_from_b.splits.keys()), list(split_from_b.splits.values())
1021-
# Ensure split can be lifted soundly (i.e., that it does not introduce fresh variables)
1022-
assert (
1023-
len(split_from_b.source_vars.difference(a.free_vars)) == 0
1024-
and len(split_from_b.target_vars.difference(split_from_b.source_vars)) == 0
1025-
)
1026-
# Create CTerms and CSubsts corresponding to the new targets of the split
1027-
new_cterms_with_constraints = [
1028-
(CTerm(a.cterm.config, a.cterm.constraints + csubst.constraints), csubst.constraint) for csubst in csubsts
1029-
]
1030-
# Generate substitutions for new targets, which all exist by construction
1031-
new_csubsts = [
1032-
not_none(a.cterm.match_with_constraint(cterm)).add_constraint(constraint)
1033-
for (cterm, constraint) in new_cterms_with_constraints
1034-
]
1035-
# Remove the node `B`, effectively removing the entire initial structure
1036-
self.remove_node(b_id)
1037-
# Create the nodes `[ A #And cond_I | I = 1..N ]`.
1038-
ai: list[NodeIdLike] = [self.create_node(cterm).id for (cterm, _) in new_cterms_with_constraints]
1039-
# Create the edges `[A #And cond_1 --M steps--> C_I | I = 1..N ]`
1040-
for i in range(len(ai)):
1041-
self.create_edge(ai[i], ci[i], a_to_b.depth, a_to_b.rules)
1042-
# Create the split `A --[cond_1, ..., cond_N]--> [A #And cond_1, ..., A #And cond_N]
1043-
self.create_split(a.id, zip(ai, new_csubsts, strict=True))
1044-
1045-
def lift_split_split(self, b_id: NodeIdLike) -> None:
1046-
"""Lift a split up a split directly preceding it, joining them into a single split.
1047-
1048-
`A --[..., cond_B, ...]--> [..., B, ...]` with `B --[cond_1, ..., cond_N]--> [C_1, ..., C_N]` becomes
1049-
`A --[..., cond_B #And cond_1, ..., cond_B #And cond_N, ...]--> [..., C_1, ..., C_N, ...]`.
1050-
Node `B` is removed.
1051-
1052-
Args:
1053-
b_id: the identifier of the node `B` of the structure
1054-
`A --[..., cond_B, ...]--> [..., B, ...]` with `B --[cond_1, ..., cond_N]--> [C_1, ..., C_N]`.
1055-
1056-
Raises:
1057-
AssertionError: If the structure in question is not in place.
1058-
"""
1059-
# Obtain splits `A --[..., cond_B, ...]--> [..., B, ...]` and
1060-
# `B --[cond_1, ..., cond_N]--> [C_1, ..., C_N]-> [C_1, ..., C_N]`
1061-
split_from_a, split_from_b = single(self.splits(target_id=b_id)), single(self.splits(source_id=b_id))
1062-
splits_from_a, splits_from_b = split_from_a.splits, split_from_b.splits
1063-
a = split_from_a.source
1064-
ci = list(splits_from_b.keys())
1065-
# Ensure split can be lifted soundly (i.e., that it does not introduce fresh variables)
1066-
assert (
1067-
len(split_from_b.source_vars.difference(a.free_vars)) == 0
1068-
and len(split_from_b.target_vars.difference(split_from_b.source_vars)) == 0
1069-
)
1070-
# Get the substitution for `B`, at the same time removing 'B' from the targets of `A`.
1071-
csubst_b = splits_from_a.pop(self._resolve(b_id))
1072-
# Generate substitutions for additional targets `C_I`, which all exist by construction;
1073-
# the constraints are cumulative, resulting in `cond_B #And cond_I`
1074-
additional_csubsts = [
1075-
not_none(a.cterm.match_with_constraint(self.node(ci).cterm))
1076-
.add_constraint(csubst_b.constraint)
1077-
.add_constraint(csubst.constraint)
1078-
for ci, csubst in splits_from_b.items()
1079-
]
1080-
# Create the targets of the new split
1081-
ci = list(splits_from_b.keys())
1082-
new_splits = zip(
1083-
list(splits_from_a.keys()) + ci, list(splits_from_a.values()) + additional_csubsts, strict=True
1084-
)
1085-
# Remove the node `B`, thereby removing the two splits as well
1086-
self.remove_node(b_id)
1087-
# Create the new split `A --[..., cond_B #And cond_1, ..., cond_B #And cond_N, ...]--> [..., C_1, ..., C_N, ...]`
1088-
self.create_split(a.id, new_splits)
1089-
1090-
def lift_splits(self) -> bool:
1091-
"""Perform all possible split liftings.
1092-
1093-
The KCFG is transformed to an equivalent in which no further split lifts are possible.
1094-
1095-
Returns:
1096-
An indicator of whether or not at least one split lift was performed.
1097-
"""
1098-
1099-
def _lift_split(finder: Callable, lifter: Callable) -> bool:
1100-
result = False
1101-
while True:
1102-
splits_to_lift = sorted(
1103-
[
1104-
node.id
1105-
for node in self.nodes
1106-
if (splits := self.splits(source_id=node.id)) != []
1107-
and (sources := finder(target_id=node.id)) != []
1108-
and (source := single(sources).source)
1109-
and (split := single(splits))
1110-
and len(split.source_vars.difference(source.free_vars)) == 0
1111-
and len(split.target_vars.difference(split.source_vars)) == 0
1112-
]
1113-
)
1114-
for id in splits_to_lift:
1115-
lifter(id)
1116-
result = True
1117-
if len(splits_to_lift) == 0:
1118-
break
1119-
return result
1120-
1121-
def _fold_lift(result: bool, finder_lifter: tuple[Callable, Callable]) -> bool:
1122-
return _lift_split(finder_lifter[0], finder_lifter[1]) or result
1123-
1124-
return reduce(_fold_lift, [(self.edges, self.lift_split_edge), (self.splits, self.lift_split_split)], False)
1125-
1126-
def minimize(self) -> None:
1127-
"""Minimize KCFG by repeatedly performing the lifting transformations.
1128-
1129-
The KCFG is transformed to an equivalent in which no further lifting transformations are possible.
1130-
The loop is designed so that each transformation is performed once in each iteration.
1131-
"""
1132-
repeat = True
1133-
while repeat:
1134-
repeat = self.lift_edges()
1135-
repeat = self.lift_splits() or repeat
1136-
1137961
def add_alias(self, alias: str, node_id: NodeIdLike) -> None:
1138962
if '@' in alias:
1139963
raise ValueError('Alias may not contain "@"')
@@ -1308,6 +1132,9 @@ def write_cfg_data(self) -> None:
13081132
self._deleted_nodes.clear()
13091133
self._created_nodes.clear()
13101134

1135+
def minimize(self) -> None:
1136+
KCFGMinimizer(self).minimize()
1137+
13111138
@staticmethod
13121139
def read_cfg_data(cfg_dir: Path) -> KCFG:
13131140
store = KCFGStore(cfg_dir)

0 commit comments

Comments
 (0)