Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 57 additions & 7 deletions pyk/src/pyk/kcfg/kcfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,30 @@ def read_cfg_data(self) -> dict[str, Any]:

return dct

def read_cfg_data_lazy(self) -> dict[str, Any]:
"""Read kcfg.json without loading node CTerms.

Node entries contain id, attrs, and the path to load the CTerm from,
but no 'cterm' key. Cover CSubsts are left as raw dicts.
"""
dct = json.loads(self.kcfg_json_path.read_text())

new_nodes = []
for node_id in dct.get('nodes') or []:
attrs = []
if node_id in dct['vacuous']:
attrs.append(KCFGNodeAttr.VACUOUS.value)
if node_id in dct['stuck']:
attrs.append(KCFGNodeAttr.STUCK.value)
new_nodes.append({'id': node_id, 'node_path': str(self.kcfg_node_path(node_id)), 'attrs': attrs})

dct['nodes'] = new_nodes

del dct['vacuous']
del dct['stuck']

return dct

def read_node_data(self, node_id: int) -> dict[str, Any]:
return json.loads(self.kcfg_node_path(node_id).read_text())

Expand Down Expand Up @@ -664,14 +688,28 @@ def to_dict(self) -> dict[str, Any]:
return {k: v for k, v in res.items() if v}

@staticmethod
def from_dict(dct: Mapping[str, Any], optimize_memory: bool = True) -> KCFG:
cfg = KCFG(optimize_memory=optimize_memory)
def from_dict(dct: Mapping[str, Any], optimize_memory: bool = True, lazy: bool = False) -> KCFG:
cfg = KCFG(optimize_memory=(optimize_memory and not lazy))

if lazy:
from pathlib import Path

for node_dict in dct.get('nodes') or []:
node = KCFG.Node.from_dict(node_dict)
cfg.add_node(node)
from .lazy import LazyCSubst, LazyNode

for node_dict in dct.get('nodes') or []:
lazy_node = LazyNode(
node_dict['id'],
frozenset(NodeAttr(a) for a in node_dict.get('attrs', [])),
Path(node_dict['node_path']),
)
cfg._nodes[lazy_node.id] = lazy_node # type: ignore[assignment]
cfg._node_id = max(cfg._node_id, lazy_node.id + 1)
else:
for node_dict in dct.get('nodes') or []:
node = KCFG.Node.from_dict(node_dict)
cfg.add_node(node)

max_id = max([node.id for node in cfg.nodes], default=0)
max_id = max(cfg._nodes.keys(), default=0)
cfg._node_id = dct.get('next', max_id + 1)

for edge_dict in dct.get('edges') or []:
Expand All @@ -683,7 +721,12 @@ def from_dict(dct: Mapping[str, Any], optimize_memory: bool = True) -> KCFG:
cfg.add_successor(merged_edge)

for cover_dict in dct.get('covers') or []:
cover = KCFG.Cover.from_dict(cover_dict, cfg._nodes)
if lazy:
src = cfg._nodes[cover_dict['source']]
tgt = cfg._nodes[cover_dict['target']]
cover = KCFG.Cover(src, tgt, LazyCSubst(cover_dict['csubst'])) # type: ignore[arg-type]
else:
cover = KCFG.Cover.from_dict(cover_dict, cfg._nodes)
cfg.add_successor(cover)

for split_dict in dct.get('splits') or []:
Expand Down Expand Up @@ -1301,6 +1344,13 @@ def read_cfg_data(cfg_dir: Path) -> KCFG:
cfg._kcfg_store = store
return cfg

@staticmethod
def read_cfg_data_lazy(cfg_dir: Path) -> KCFG:
store = KCFGStore(cfg_dir)
cfg = KCFG.from_dict(store.read_cfg_data_lazy(), lazy=True)
cfg._kcfg_store = store
return cfg

@staticmethod
def read_node_data(cfg_dir: Path, node_id: int) -> KCFG.Node:
store = KCFGStore(cfg_dir)
Expand Down
151 changes: 151 additions & 0 deletions pyk/src/pyk/kcfg/lazy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""Lazy loading stubs for memory-efficient proof display.

These stubs duck-type the real KCFG classes, deferring heavy data loading
(node CTerms, cover/split CSubsts) until actually accessed for printing.
"""

from __future__ import annotations

import json
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from pathlib import Path
from typing import Any

from ..cterm import CSubst, CTerm
from ..kast.inner import KInner
from .kcfg import KCFG


class LazyNode:
"""Duck-types KCFG.Node. Loads CTerm from disk on first .cterm access."""

id: int
attrs: frozenset
_node_path: Path
_cterm: CTerm | None

def __init__(self, id: int, attrs: frozenset, node_path: Path) -> None:
self.id = id
self.attrs = attrs
self._node_path = node_path
self._cterm = None

@property
def cterm(self) -> CTerm:
if self._cterm is None:
from ..cterm import CTerm

node_dict = json.loads(self._node_path.read_text())
self._cterm = CTerm.from_dict(node_dict['cterm'])
return self._cterm

def evict(self) -> None:
"""Release the loaded CTerm from memory."""
self._cterm = None

def __eq__(self, other: object) -> bool:
if isinstance(other, LazyNode):
return self.id == other.id
# Also compare with real KCFG.Node
return hasattr(other, 'id') and self.id == other.id

def __hash__(self) -> int:
return hash(self.id)

def __lt__(self, other: object) -> bool:
if hasattr(other, 'id'):
return self.id < other.id
return NotImplemented

def __le__(self, other: object) -> bool:
if hasattr(other, 'id'):
return self.id <= other.id
return NotImplemented


class APRProofStub:
"""Lightweight stub for APRProof — answers proof-level queries without loading the full proof.

Duck-types enough of APRProof for APRProofNodePrinter.node_attrs() to work.
Uses proof.json metadata + the KCFG for graph queries.
"""

def __init__(self, proof_dict: dict[str, Any], kcfg: KCFG) -> None:
self.init = int(proof_dict['init'])
self.target = int(proof_dict['target'])
self._terminal_ids = set(proof_dict.get('terminal') or [])
self._bounded_ids = set(proof_dict.get('bounded') or [])
self._refuted_ids = {int(k) for k in (proof_dict.get('node_refutations') or {}).keys()}
self.kcfg = kcfg

def _resolve(self, node_id: int) -> int:
return node_id

def is_init(self, node_id: int) -> bool:
return node_id == self.init

def is_target(self, node_id: int) -> bool:
return node_id == self.target

def is_terminal(self, node_id: int) -> bool:
return node_id in self._terminal_ids

def is_explorable(self, node_id: int) -> bool:
return (
self.kcfg.is_leaf(node_id)
and not self.is_terminal(node_id)
and not self.kcfg.is_stuck(node_id)
and not self.kcfg.is_vacuous(node_id)
)

def is_pending(self, node_id: int) -> bool:
return (
self.is_explorable(node_id)
and not self.is_target(node_id)
and not self.is_refuted(node_id)
and not self.is_bounded(node_id)
)

def is_refuted(self, node_id: int) -> bool:
return node_id in self._refuted_ids

def is_bounded(self, node_id: int) -> bool:
return node_id in self._bounded_ids


class LazyCSubst:
"""Duck-types CSubst. Loads from a JSON dict on first access."""

_raw: dict[str, Any]
_csubst: CSubst | None

def __init__(self, raw_dict: dict[str, Any]) -> None:
self._raw: dict[str, Any] = raw_dict
self._csubst = None

def _load(self) -> CSubst:
if self._csubst is None:
from ..cterm import CSubst

self._csubst = CSubst.from_dict(self._raw)
return self._csubst

@property
def constraints(self) -> tuple:
return self._load().constraints

@property
def subst(self) -> object:
return self._load().subst

def pred(self, *args: Any, **kwargs: Any) -> KInner:
return self._load().pred(*args, **kwargs)

def to_dict(self) -> dict:
return self._load().to_dict()

def evict(self) -> None:
"""Release the loaded CSubst from memory, keep raw dict for reload."""
self._csubst = None
66 changes: 43 additions & 23 deletions pyk/src/pyk/kcfg/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from .kcfg import KCFG

if TYPE_CHECKING:
from collections.abc import Iterable
from collections.abc import Iterable, Iterator
from pathlib import Path
from typing import Final

Expand Down Expand Up @@ -325,8 +325,30 @@ def show(
omit_cells: Iterable[str] = (),
module_name: str | None = None,
) -> list[str]:
res_lines: list[str] = []
res_lines += self.pretty(cfg, minimize=minimize)
return list(
self.show_iter(
cfg,
nodes=nodes,
node_deltas=node_deltas,
to_module=to_module,
minimize=minimize,
omit_cells=omit_cells,
module_name=module_name,
)
)

def show_iter(
self,
cfg: KCFG,
nodes: Iterable[NodeIdLike] = (),
node_deltas: Iterable[tuple[NodeIdLike, NodeIdLike]] = (),
to_module: bool = False,
minimize: bool = True,
omit_cells: Iterable[str] = (),
module_name: str | None = None,
) -> Iterator[str]:
"""Yield proof show output line-by-line, avoiding memory accumulation."""
yield from self.pretty(cfg, minimize=minimize)

nodes_printed = False

Expand All @@ -336,12 +358,12 @@ def show(
kast = KCFGShow.hide_cells(kast, omit_cells)
if minimize:
kast = minimize_term(kast)
res_lines.append('')
res_lines.append('')
res_lines.append(f'Node {node_id}:')
res_lines.append('')
res_lines.append(self.pretty_printer.print(kast))
res_lines.append('')
yield ''
yield ''
yield f'Node {node_id}:'
yield ''
yield self.pretty_printer.print(kast)
yield ''

for node_id_1, node_id_2 in node_deltas:
nodes_printed = True
Expand All @@ -350,23 +372,21 @@ def show(
config_delta = push_down_rewrites(KRewrite(config_1, config_2))
if minimize:
config_delta = minimize_term(config_delta)
res_lines.append('')
res_lines.append('')
res_lines.append(f'State Delta {node_id_1} => {node_id_2}:')
res_lines.append('')
res_lines.append(self.pretty_printer.print(config_delta))
res_lines.append('')

if not (nodes_printed):
res_lines.append('')
res_lines.append('')
res_lines.append('')
yield ''
yield ''
yield f'State Delta {node_id_1} => {node_id_2}:'
yield ''
yield self.pretty_printer.print(config_delta)
yield ''

if not nodes_printed:
yield ''
yield ''
yield ''

if to_module:
module = self.to_module(cfg, module_name, omit_cells=omit_cells)
res_lines.append(self.pretty_printer.print(module))

return res_lines
yield self.pretty_printer.print(module)

def dot(self, kcfg: KCFG) -> Digraph:
def _short_label(label: str) -> str:
Expand Down
Loading
Loading