Skip to content

Commit 04e8234

Browse files
committed
feat: add --to-module, --minimize-proof, and --add-module CLI options
Add CLI options for semantics summarization evaluation: 1. `--to-module <file>` for `kmir show`: - Export proof KCFG as K module to specified file - Supports .json (KFlatModule JSON format) and .k (K text format) 2. `--minimize-proof` for `kmir show`: - Minimize the proof KCFG before displaying or exporting - Saves minimized proof back to disk 3. `--add-module <file>` for `kmir prove-rs`: - Load K module file and pass to APRProver via extra_module parameter - Currently supports JSON format only Note: The --add-module feature uses pyk's native extra_module support in APRProver. However, rules from KCFGShow.to_module() have complex partial configurations that may cause sort injection failures during Kore conversion.
1 parent 24a387b commit 04e8234

File tree

7 files changed

+1205
-15
lines changed

7 files changed

+1205
-15
lines changed

kmir/src/kmir/__main__.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ def _kmir_show(opts: ShowOpts) -> None:
9292
kmir = KMIR(HASKELL_DEF_DIR, LLVM_LIB_DIR)
9393
proof = APRProof.read_proof_data(opts.proof_dir, opts.id)
9494

95+
# Minimize proof KCFG if requested
96+
if opts.minimize_proof:
97+
_LOGGER.info('Minimizing proof KCFG...')
98+
proof.minimize_kcfg()
99+
proof.write_proof_data()
100+
_LOGGER.info('Proof KCFG minimized and saved')
101+
95102
# Use custom KMIR printer by default, switch to standard printer if requested
96103
if opts.use_default_printer:
97104
printer = PrettyPrinter(kmir.definition)
@@ -119,6 +126,7 @@ def _kmir_show(opts: ShowOpts) -> None:
119126
nodes=opts.nodes or (),
120127
node_deltas=effective_node_deltas,
121128
omit_cells=tuple(all_omit_cells),
129+
to_module=opts.to_module is not None,
122130
)
123131
if opts.statistics:
124132
if lines and lines[-1] != '':
@@ -132,7 +140,36 @@ def _kmir_show(opts: ShowOpts) -> None:
132140
lines.append('')
133141
lines.extend(render_leaf_k_cells(proof, node_printer.cterm_show))
134142

135-
print('\n'.join(lines))
143+
# Handle --to-module output
144+
if opts.to_module:
145+
import json
146+
147+
from pyk.kast.manip import remove_generated_cells
148+
from pyk.kast.outer import KRule
149+
150+
# Generate K module using KCFG.to_module with defunc_with for proper function inlining
151+
module_name = proof.id.upper().replace('.', '-').replace('_', '-') + '-SUMMARY'
152+
k_module = proof.kcfg.to_module(module_name=module_name, defunc_with=kmir.definition)
153+
154+
if opts.to_module.suffix == '.json':
155+
# JSON format for --add-module: keep <generatedTop> for Kore conversion
156+
# Note: We don't use minimize_rule_like here because it creates partial configs
157+
# with dots that cannot be converted back to Kore
158+
opts.to_module.write_text(json.dumps(k_module.to_dict(), indent=2))
159+
else:
160+
# K text format for human readability: remove <generatedTop> and <generatedCounter>
161+
def _process_sentence(sent): # type: ignore[no-untyped-def]
162+
if isinstance(sent, KRule):
163+
sent = sent.let(body=remove_generated_cells(sent.body))
164+
return sent
165+
166+
k_module_readable = k_module.let(sentences=[_process_sentence(sent) for sent in k_module.sentences])
167+
k_module_text = kmir.pretty_print(k_module_readable)
168+
opts.to_module.write_text(k_module_text)
169+
_LOGGER.info(f'Module written to: {opts.to_module}')
170+
print(f'Module written to: {opts.to_module}')
171+
else:
172+
print('\n'.join(lines))
136173

137174

138175
def _kmir_prune(opts: PruneOpts) -> None:
@@ -410,6 +447,17 @@ def _arg_parser() -> ArgumentParser:
410447
)
411448

412449
show_parser.add_argument('--rules', metavar='EDGES', help='Comma separated list of edges in format "source:target"')
450+
show_parser.add_argument(
451+
'--to-module',
452+
type=Path,
453+
metavar='FILE',
454+
help='Output path for K module file (.k for readable, .json for --add-module)',
455+
)
456+
show_parser.add_argument(
457+
'--minimize-proof',
458+
action='store_true',
459+
help='Minimize the proof KCFG before exporting to module',
460+
)
413461

414462
command_parser.add_parser(
415463
'view', help='View proof information', parents=[kcli_args.logging_args, proof_args, display_args]
@@ -443,6 +491,12 @@ def _arg_parser() -> ArgumentParser:
443491
prove_rs_parser.add_argument(
444492
'--start-symbol', type=str, metavar='SYMBOL', default='main', help='Symbol name to begin execution from'
445493
)
494+
prove_rs_parser.add_argument(
495+
'--add-module',
496+
type=Path,
497+
metavar='FILE',
498+
help='K module file to include (.json format from --to-module)',
499+
)
446500

447501
link_parser = command_parser.add_parser(
448502
'link', help='Link together 2 or more SMIR JSON files', parents=[kcli_args.logging_args]
@@ -530,6 +584,7 @@ def _parse_args(ns: Namespace) -> KMirOpts:
530584
break_every_terminator=ns.break_every_terminator,
531585
break_every_step=ns.break_every_step,
532586
terminate_on_thunk=ns.terminate_on_thunk,
587+
add_module=ns.add_module,
533588
)
534589
case 'link':
535590
return LinkOpts(

kmir/src/kmir/kmir.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ def cut_point_rules(
119119

120120
@staticmethod
121121
def from_kompiled_kore(
122-
smir_info: SMIRInfo, target_dir: Path, bug_report: Path | None = None, symbolic: bool = True
122+
smir_info: SMIRInfo,
123+
target_dir: Path,
124+
bug_report: Path | None = None,
125+
symbolic: bool = True,
126+
extra_module: Path | None = None,
123127
) -> KMIR:
124128
from .kompile import kompile_smir
125129

@@ -128,6 +132,7 @@ def from_kompiled_kore(
128132
target_dir=target_dir,
129133
bug_report=bug_report,
130134
symbolic=symbolic,
135+
extra_module=extra_module,
131136
)
132137
return kompiled_smir.create_kmir(bug_report_file=bug_report)
133138

@@ -213,7 +218,11 @@ def prove_rs(opts: ProveRSOpts) -> APRProof:
213218

214219
smir_info = SMIRInfo.from_file(target_path / 'smir.json')
215220
kmir = KMIR.from_kompiled_kore(
216-
smir_info, symbolic=True, bug_report=opts.bug_report, target_dir=target_path
221+
smir_info,
222+
symbolic=True,
223+
bug_report=opts.bug_report,
224+
target_dir=target_path,
225+
extra_module=opts.add_module,
217226
)
218227
else:
219228
_LOGGER.info(f'Constructing initial proof: {label}')
@@ -237,7 +246,11 @@ def prove_rs(opts: ProveRSOpts) -> APRProof:
237246
_LOGGER.debug(f'Missing-body function symbols (first 5): {missing_body_syms[:5]}')
238247

239248
kmir = KMIR.from_kompiled_kore(
240-
smir_info, symbolic=True, bug_report=opts.bug_report, target_dir=target_path
249+
smir_info,
250+
symbolic=True,
251+
bug_report=opts.bug_report,
252+
target_dir=target_path,
253+
extra_module=opts.add_module,
241254
)
242255

243256
apr_proof = kmir.apr_proof_from_smir(
@@ -267,7 +280,11 @@ def prove_rs(opts: ProveRSOpts) -> APRProof:
267280
)
268281

269282
with kmir.kcfg_explore(label, terminate_on_thunk=opts.terminate_on_thunk) as kcfg_explore:
270-
prover = APRProver(kcfg_explore, execute_depth=opts.max_depth, cut_point_rules=cut_point_rules)
283+
prover = APRProver(
284+
kcfg_explore,
285+
execute_depth=opts.max_depth,
286+
cut_point_rules=cut_point_rules,
287+
)
271288
prover.advance_proof(
272289
apr_proof,
273290
max_iterations=opts.max_iterations,

kmir/src/kmir/kompile.py

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,46 @@ def _digest_file(target_dir: Path) -> Path:
9191
return target_dir / 'smir-digest.json'
9292

9393

94+
def _load_extra_module_rules(kmir: KMIR, module_path: Path) -> list[Sentence]:
95+
"""Load a K module from JSON and convert rules to Kore axioms.
96+
97+
Args:
98+
kmir: KMIR instance with the definition
99+
module_path: Path to JSON module file (from --to-module output.json)
100+
101+
Returns:
102+
List of Kore axioms converted from the module rules
103+
"""
104+
from pyk.kast.outer import KFlatModule, KRule
105+
from pyk.konvert import krule_to_kore
106+
107+
_LOGGER.info(f'Loading extra module rules: {module_path}')
108+
109+
if module_path.suffix != '.json':
110+
_LOGGER.warning(f'Only JSON format is supported for --add-module: {module_path}')
111+
return []
112+
113+
module_dict = json.loads(module_path.read_text())
114+
k_module = KFlatModule.from_dict(module_dict)
115+
116+
axioms: list[Sentence] = []
117+
for sentence in k_module.sentences:
118+
if isinstance(sentence, KRule):
119+
try:
120+
axiom = krule_to_kore(kmir.definition, sentence)
121+
axioms.append(axiom)
122+
except Exception as e:
123+
_LOGGER.warning(f'Failed to convert rule to Kore: {e}')
124+
125+
return axioms
126+
127+
94128
def kompile_smir(
95129
smir_info: SMIRInfo,
96130
target_dir: Path,
97131
bug_report: Path | None = None,
98132
symbolic: bool = True,
133+
extra_module: Path | None = None,
99134
) -> KompiledSMIR:
100135
kompile_digest: KompileDigest | None = None
101136
try:
@@ -120,8 +155,18 @@ def kompile_smir(
120155
target_dir.mkdir(parents=True, exist_ok=True)
121156

122157
kmir = KMIR(HASKELL_DEF_DIR)
123-
rules = make_kore_rules(kmir, smir_info)
124-
_LOGGER.info(f'Generated {len(rules)} function equations to add to `definition.kore')
158+
smir_rules: list[Sentence] = list(make_kore_rules(kmir, smir_info))
159+
_LOGGER.info(f'Generated {len(smir_rules)} function equations to add to `definition.kore')
160+
161+
# Load and convert extra module rules if provided
162+
# These are kept separate because LLVM backend doesn't support configuration rewrites
163+
extra_rules: list[Sentence] = []
164+
if extra_module is not None:
165+
extra_rules = _load_extra_module_rules(kmir, extra_module)
166+
_LOGGER.info(f'Added {len(extra_rules)} rules from extra module: {extra_module}')
167+
168+
# Combined rules for Haskell backend (supports both function equations and rewrites)
169+
all_rules = smir_rules + extra_rules
125170

126171
if symbolic:
127172
# Create output directories
@@ -131,11 +176,12 @@ def kompile_smir(
131176
target_llvmdt_path.mkdir(parents=True, exist_ok=True)
132177
target_hs_path.mkdir(parents=True, exist_ok=True)
133178

134-
# Process LLVM definition
179+
# Process LLVM definition (only SMIR rules, not extra module rules)
180+
# Extra module rules are configuration rewrites that LLVM backend doesn't support
135181
_LOGGER.info('Writing LLVM definition file')
136182
llvm_def_file = LLVM_LIB_DIR / 'definition.kore'
137183
llvm_def_output = target_llvm_lib_path / 'definition.kore'
138-
_insert_rules_and_write(llvm_def_file, rules, llvm_def_output)
184+
_insert_rules_and_write(llvm_def_file, smir_rules, llvm_def_output)
139185

140186
# Run llvm-kompile-matching and llvm-kompile for LLVM
141187
# TODO use pyk to do this if possible (subprocess wrapper, maybe llvm-kompile itself?)
@@ -161,10 +207,10 @@ def kompile_smir(
161207
check=True,
162208
)
163209

164-
# Process Haskell definition
210+
# Process Haskell definition (includes both SMIR rules and extra module rules)
165211
_LOGGER.info('Writing Haskell definition file')
166212
hs_def_file = HASKELL_DEF_DIR / 'definition.kore'
167-
_insert_rules_and_write(hs_def_file, rules, target_hs_path / 'definition.kore')
213+
_insert_rules_and_write(hs_def_file, all_rules, target_hs_path / 'definition.kore')
168214

169215
# Copy all files except definition.kore and binary from HASKELL_DEF_DIR to out/hs
170216
_LOGGER.info('Copying other artefacts into HS output directory')
@@ -183,11 +229,11 @@ def kompile_smir(
183229
_LOGGER.info(f'Creating directory {target_llvmdt_path}')
184230
target_llvmdt_path.mkdir(parents=True, exist_ok=True)
185231

186-
# Process LLVM definition
232+
# Process LLVM definition (only SMIR rules for concrete execution)
187233
_LOGGER.info('Writing LLVM definition file')
188234
llvm_def_file = LLVM_LIB_DIR / 'definition.kore'
189235
llvm_def_output = target_llvm_path / 'definition.kore'
190-
_insert_rules_and_write(llvm_def_file, rules, llvm_def_output)
236+
_insert_rules_and_write(llvm_def_file, smir_rules, llvm_def_output)
191237

192238
import subprocess
193239

kmir/src/kmir/options.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ class ProveRSOpts(ProveOpts):
114114
save_smir: bool
115115
smir: bool
116116
start_symbol: str
117+
add_module: Path | None
117118

118119
def __init__(
119120
self,
@@ -143,6 +144,7 @@ def __init__(
143144
break_every_terminator: bool = False,
144145
break_every_step: bool = False,
145146
terminate_on_thunk: bool = False,
147+
add_module: Path | None = None,
146148
) -> None:
147149
self.rs_file = rs_file
148150
self.proof_dir = Path(proof_dir).resolve() if proof_dir is not None else None
@@ -170,6 +172,7 @@ def __init__(
170172
self.break_every_terminator = break_every_terminator
171173
self.break_every_step = break_every_step
172174
self.terminate_on_thunk = terminate_on_thunk
175+
self.add_module = add_module
173176

174177

175178
@dataclass
@@ -204,6 +207,8 @@ class ShowOpts(DisplayOpts):
204207
use_default_printer: bool
205208
statistics: bool
206209
leaves: bool
210+
to_module: Path | None
211+
minimize_proof: bool
207212

208213
def __init__(
209214
self,
@@ -221,12 +226,16 @@ def __init__(
221226
use_default_printer: bool = False,
222227
statistics: bool = False,
223228
leaves: bool = False,
229+
to_module: Path | None = None,
230+
minimize_proof: bool = False,
224231
) -> None:
225232
super().__init__(proof_dir, id, full_printer, smir_info, omit_current_body)
226233
self.omit_static_info = omit_static_info
227234
self.use_default_printer = use_default_printer
228235
self.statistics = statistics
229236
self.leaves = leaves
237+
self.to_module = to_module
238+
self.minimize_proof = minimize_proof
230239
self.nodes = tuple(int(n.strip()) for n in nodes.split(',')) if nodes is not None else None
231240

232241
def _parse_pairs(text: str | None) -> tuple[tuple[int, int], ...] | None:

0 commit comments

Comments
 (0)