Skip to content

Commit dd703aa

Browse files
authored
Add --rules and node-deltas-pro optios for kmir show (#649)
- Added options for displaying additional node deltas and rules in `kmir show`. - Updated the README to reflect new command usage and examples. - Removed the deprecated `show-rules` command and integrated its functionality into `kmir show`. - Introduced a utility function to render rules as Markdown links for better readability.
1 parent 32ff444 commit dd703aa

File tree

4 files changed

+145
-62
lines changed

4 files changed

+145
-62
lines changed

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ uv --project kmir run kmir show proof_id --proof-dir ./proof_dir --nodes "1,2,3"
122122
# Show node deltas (transitions between specific nodes)
123123
uv --project kmir run kmir show proof_id --proof-dir ./proof_dir --node-deltas "1:2,3:4"
124124

125+
# Show additional deltas after the main output, and also print rules for those edges
126+
uv --project kmir run kmir show proof_id --proof-dir ./proof_dir --node-deltas "1:2" --node-deltas-pro "3:4"
127+
125128
# Display full node information (default is compact)
126129
uv --project kmir run kmir show proof_id --proof-dir ./proof_dir --full-printer
127130

@@ -143,9 +146,9 @@ uv --project kmir run kmir show proof_id --proof-dir ./proof_dir --full-printer
143146
uv --project kmir run kmir view proof_id --proof-dir ./proof_dir --verbose
144147
```
145148

146-
**`kmir show-rules`** - Show rules applied between nodes
149+
**Rules within `kmir show`** - Show rules applied between nodes
147150
```bash
148-
uv --project kmir run kmir show-rules proof_id source_node target_node --proof-dir ./proof_dir
151+
uv --project kmir run kmir show proof_id --proof-dir ./proof_dir --rules "SOURCE:TARGET[,SOURCE:TARGET...]"
149152
```
150153

151154
### Recommended Workflow
@@ -178,7 +181,7 @@ uv --project kmir run kmir show-rules proof_id source_node target_node --proof-d
178181

179182
4. **Analyze Rules**:
180183
```bash
181-
uv --project kmir run kmir show-rules proof_id 1 3 --proof-dir ./proof_dir
184+
uv --project kmir run kmir show proof_id --proof-dir ./proof_dir --rules "1:3"
182185
```
183186

184187
### Advanced Show Usage Examples
@@ -219,6 +222,8 @@ Most commands support:
219222
**`kmir show` specific options:**
220223
- `--nodes NODES`: Comma separated list of node IDs to show (e.g., "1,2,3")
221224
- `--node-deltas DELTAS`: Comma separated list of node deltas in format "source:target" (e.g., "1:2,3:4")
225+
- `--node-deltas-pro DELTAS`: Additional node deltas (same format as `--node-deltas`). Equivalent to "print the corresponding deltas again, and automatically print the rules for these edges".
226+
- `--rules EDGES`: Comma separated list of edges in format "source:target". Prints rules for each edge in Markdown link format `[label](file:///abs/path#LstartLine)` when available
222227
- `--omit-cells CELLS`: Comma separated list of cell names to omit from output
223228
- `--full-printer`: Display the full node in output (default is compact)
224229
- `--no-omit-static-info`: Display static information cells (functions, start-symbol, types, adt-to-ty)

kmir/src/kmir/__main__.py

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@
2727
PruneOpts,
2828
RunOpts,
2929
ShowOpts,
30-
ShowRulesOpts,
3130
ViewOpts,
3231
)
3332
from .parse.parser import parse_json
3433
from .smir import SMIRInfo, Ty
34+
from .utils import render_rules
3535

3636
if TYPE_CHECKING:
3737
from argparse import Namespace
@@ -154,32 +154,23 @@ def _kmir_show(opts: ShowOpts) -> None:
154154
node_printer = KMIRAPRNodePrinter(cterm_show, proof, opts)
155155
shower = APRProofShow(kmir.definition, node_printer=node_printer)
156156
shower.kcfg_show.pretty_printer = printer
157+
effective_node_deltas = tuple(sorted({*(opts.node_deltas or ()), *(opts.node_deltas_pro or ())}))
158+
effective_rule_edges = tuple(sorted({*(opts.rules or ()), *(opts.node_deltas_pro or ())}))
159+
157160
_LOGGER.info(
158-
f'Showing {proof.id} with\n nodes: {opts.nodes},\n node_deltas: {opts.node_deltas},\n omit_cells: {tuple(all_omit_cells)}'
161+
f'Showing {proof.id} with\n nodes: {opts.nodes},\n node_deltas: {effective_node_deltas},\n omit_cells: {tuple(all_omit_cells)}'
159162
)
160163
lines = shower.show(
161164
proof,
162165
nodes=opts.nodes or (),
163-
node_deltas=opts.node_deltas or (),
166+
node_deltas=effective_node_deltas,
164167
omit_cells=tuple(all_omit_cells),
165168
)
166-
print('\n'.join(lines))
167-
169+
if effective_rule_edges:
170+
lines.append('# Rules: ')
171+
lines.extend(render_rules(proof, effective_rule_edges))
168172

169-
def _kmir_show_rules(opts: ShowRulesOpts) -> None:
170-
"""Show rules applied on the edge from source to target node."""
171-
proof = APRProof.read_proof_data(opts.proof_dir, opts.id)
172-
edge = proof.kcfg.edge(opts.source, opts.target)
173-
if edge is None:
174-
print(f'Error: No edge found from node {opts.source} to node {opts.target}')
175-
return
176-
rules = edge.rules
177-
print(f'Rules applied on edge {opts.source} -> {opts.target}:')
178-
print(f'Total rules: {len(rules)}')
179-
print('-' * 80)
180-
for rule in rules:
181-
print(rule)
182-
print('-' * 80)
173+
print('\n'.join(lines))
183174

184175

185176
def _kmir_prune(opts: PruneOpts) -> None:
@@ -221,8 +212,6 @@ def kmir(args: Sequence[str]) -> None:
221212
_kmir_view(opts)
222213
case ShowOpts():
223214
_kmir_show(opts)
224-
case ShowRulesOpts():
225-
_kmir_show_rules(opts)
226215
case PruneOpts():
227216
_kmir_prune(opts)
228217
case ProveRSOpts():
@@ -318,6 +307,9 @@ def _arg_parser() -> ArgumentParser:
318307
show_parser.add_argument(
319308
'--node-deltas', metavar='DELTAS', help='Comma separated list of node deltas in format "source:target"'
320309
)
310+
show_parser.add_argument(
311+
'--node-deltas-pro', metavar='DELTAS', help='Extra node deltas (printed after main output)'
312+
)
321313
show_parser.add_argument(
322314
'--omit-cells', metavar='CELLS', help='Comma separated list of cell names to omit from output'
323315
)
@@ -336,11 +328,7 @@ def _arg_parser() -> ArgumentParser:
336328
help='Use standard PrettyPrinter instead of custom KMIR printer',
337329
)
338330

339-
show_rules_parser = command_parser.add_parser(
340-
'show-rules', help='Show rules applied on a specific edge', parents=[kcli_args.logging_args, proof_args]
341-
)
342-
show_rules_parser.add_argument('source', type=int, metavar='SOURCE', help='Source node ID')
343-
show_rules_parser.add_argument('target', type=int, metavar='TARGET', help='Target node ID')
331+
show_parser.add_argument('--rules', metavar='EDGES', help='Comma separated list of edges in format "source:target"')
344332

345333
command_parser.add_parser(
346334
'view', help='View proof information', parents=[kcli_args.logging_args, proof_args, display_args]
@@ -411,17 +399,12 @@ def _parse_args(ns: Namespace) -> KMirOpts:
411399
omit_current_body=ns.omit_current_body,
412400
nodes=ns.nodes,
413401
node_deltas=ns.node_deltas,
402+
node_deltas_pro=ns.node_deltas_pro,
403+
rules=ns.rules,
414404
omit_cells=ns.omit_cells,
415405
omit_static_info=ns.omit_static_info,
416406
use_default_printer=ns.use_default_printer,
417407
)
418-
case 'show-rules':
419-
return ShowRulesOpts(
420-
proof_dir=Path(ns.proof_dir),
421-
id=ns.id,
422-
source=ns.source,
423-
target=ns.target,
424-
)
425408
case 'view':
426409
proof_dir = Path(ns.proof_dir)
427410
return ViewOpts(

kmir/src/kmir/options.py

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ def __init__(
153153
class ShowOpts(DisplayOpts):
154154
nodes: tuple[int, ...] | None
155155
node_deltas: tuple[tuple[int, int], ...] | None
156+
node_deltas_pro: tuple[tuple[int, int], ...] | None
157+
rules: tuple[tuple[int, int], ...] | None
156158
omit_cells: tuple[str, ...] | None
157159
omit_static_info: bool
158160
use_default_printer: bool
@@ -166,6 +168,8 @@ def __init__(
166168
omit_current_body: bool = True,
167169
nodes: str | None = None,
168170
node_deltas: str | None = None,
171+
node_deltas_pro: str | None = None,
172+
rules: str | None = None,
169173
omit_cells: str | None = None,
170174
omit_static_info: bool = True,
171175
use_default_printer: bool = False,
@@ -174,15 +178,20 @@ def __init__(
174178
self.omit_static_info = omit_static_info
175179
self.use_default_printer = use_default_printer
176180
self.nodes = tuple(int(n.strip()) for n in nodes.split(',')) if nodes is not None else None
177-
if node_deltas is not None:
178-
deltas = []
179-
for delta in node_deltas.split(','):
181+
182+
def _parse_pairs(text: str | None) -> tuple[tuple[int, int], ...] | None:
183+
if text is None:
184+
return None
185+
pairs: list[tuple[int, int]] = []
186+
for delta in text.split(','):
180187
parts = delta.strip().split(':')
181-
if len(parts) == 2:
182-
deltas.append((int(parts[0].strip()), int(parts[1].strip())))
183-
self.node_deltas = tuple(deltas)
184-
else:
185-
self.node_deltas = None
188+
if len(parts) == 2 and parts[0].strip() and parts[1].strip():
189+
pairs.append((int(parts[0].strip()), int(parts[1].strip())))
190+
return tuple(pairs)
191+
192+
self.node_deltas = _parse_pairs(node_deltas)
193+
self.node_deltas_pro = _parse_pairs(node_deltas_pro)
194+
self.rules = _parse_pairs(rules)
186195

187196
static_info_cells = ('<functions>', '<start-symbol>', '<types>', '<adt-to-ty>')
188197

@@ -198,24 +207,6 @@ def __init__(
198207
class ViewOpts(DisplayOpts): ...
199208

200209

201-
@dataclass
202-
class ShowRulesOpts(ProofOpts):
203-
source: int
204-
target: int
205-
206-
def __init__(
207-
self,
208-
proof_dir: Path | str,
209-
id: str,
210-
source: int,
211-
target: int,
212-
) -> None:
213-
self.proof_dir = Path(proof_dir).resolve() if proof_dir is not None else None
214-
self.id = id
215-
self.source = source
216-
self.target = target
217-
218-
219210
@dataclass
220211
class PruneOpts(ProofOpts):
221212
node_id: int

kmir/src/kmir/utils.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from pathlib import Path
5+
from typing import TYPE_CHECKING, Sequence
6+
7+
if TYPE_CHECKING:
8+
from pyk.proof.reachability import APRProof
9+
10+
11+
def _rule_to_markdown_link(rule: object) -> str:
12+
"""Render a single rule as a Markdown file link if possible.
13+
14+
The function tries, in order:
15+
- Parse the first line of str(rule) for pattern: <id>:/abs/path:(startLine, startCol, endLine, endCol)
16+
- Use rule attributes 'source' and 'location' when available
17+
18+
Fallback to the first line of str(rule) if no path can be determined.
19+
"""
20+
text_first = str(rule).splitlines()[0]
21+
22+
# 1) Parse textual representation first (most robust across backends)
23+
m_text = re.match(r'^[^:]+:(/[^:]+):\((\d+)\s*,\s*\d+\s*,\s*\d+\s*,\s*\d+\)\s*$', text_first)
24+
if m_text:
25+
file_path = m_text.group(1)
26+
start_line = int(m_text.group(2))
27+
uri = Path(file_path).resolve().as_uri()
28+
label_text = f'{Path(file_path).name}:{start_line}'
29+
return f'[{label_text}]({uri}#L{start_line})'
30+
31+
# 2) Try attributes on the rule object (when present)
32+
try:
33+
att = getattr(rule, 'att', None)
34+
source_file: str | None = None
35+
if att is not None:
36+
get = getattr(att, 'get', None)
37+
if callable(get):
38+
source_file = get('source')
39+
loc = get('location')
40+
else:
41+
try:
42+
source_file = att['source'] # type: ignore[index]
43+
except Exception:
44+
source_file = None
45+
try:
46+
loc = att['location'] # type: ignore[index]
47+
except Exception:
48+
loc = None
49+
50+
if isinstance(loc, str):
51+
# Format 1: startLine:startCol-endLine:endCol
52+
m = re.match(r'^(\d+):(\d+)-(\d+):(\d+)$', loc)
53+
if m:
54+
start_line = int(m.group(1))
55+
else:
56+
# Format 2: (startLine, startCol, endLine, endCol)
57+
m2 = re.match(r'^\(\s*(\d+)\s*,\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$', loc)
58+
if m2:
59+
start_line = int(m2.group(1))
60+
61+
if source_file is not None:
62+
uri = Path(source_file).resolve().as_uri()
63+
line_anchor = f'#L{start_line}' if start_line is not None else ''
64+
label_text = (
65+
f'{Path(source_file).name}:{start_line}' if start_line is not None else Path(source_file).name
66+
)
67+
return f'[{label_text}]({uri}{line_anchor})'
68+
except Exception:
69+
pass
70+
71+
# 3) Fallback: raw first line
72+
return text_first
73+
74+
75+
def render_rules(proof: APRProof, edges: Sequence[tuple[int, int]]) -> list[str]:
76+
"""Render rules for a collection of edges as markdown text lines.
77+
78+
- proof: APRProof containing the kcfg with edges
79+
- edges: iterable of (src, dst)
80+
"""
81+
# Deduplicate while preserving a stable ordering
82+
seen: set[tuple[int, int]] = set()
83+
ordered_unique_edges: list[tuple[int, int]] = []
84+
for e in edges:
85+
if e not in seen:
86+
seen.add(e)
87+
ordered_unique_edges.append(e)
88+
89+
lines: list[str] = []
90+
for src, dst in ordered_unique_edges:
91+
edge = proof.kcfg.edge(src, dst)
92+
if edge is None:
93+
lines.append(f'Rules applied on edge {src} -> {dst}:')
94+
lines.append('No edge found')
95+
continue
96+
applied = edge.rules
97+
lines.append(f'Rules applied on edge {src} -> {dst}:')
98+
lines.append(f'Total rules: {len(applied)}')
99+
lines.append('-' * 80)
100+
for rule in applied:
101+
lines.append(_rule_to_markdown_link(rule))
102+
lines.append('-' * 80)
103+
104+
return lines

0 commit comments

Comments
 (0)