Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 8a39504

Browse files
nwatson22rv-auditortothtamas28
authored andcommittedApr 9, 2024
Improve default argument handling (runtimeverification/pyk#916)
Closes runtimeverification/pyk#885. Overhauls the way command line argument parsing and parameter passing is done. When building a pyk-based command-line tool, for each subcommand of the tool, extend `class Command` . This subclass should contain all information about what arguments are accepted by that command (through providing its own arguments or inheriting from other `Options` classes), the default values of those arguments, the name of the command, and the help string for the command, as static fields. The values of those options for a specific invocation of that command are stored as non-static fields of a `Command`. The `Command` subclass contains the code that runs when that command is called in `exec()`. In addition, default values of arguments inherited from other `Options` classes can be overridden in the subclass. The `CLI` class manages the tool's CLI options. It is constructed by passing in the name of every command subclass to be included in the tool. It can then build the whole `ArgumentParser` for the tool and process the arguments to instantiate a new `*Command` of the correct type and with the correct arguments. Advantages: - All information about a subcommand is consolidated into one place - Default values specified in only one place - Commands only have to be listed once, when instantiating `CLI` - Setting up an argument parser with all subcommands is done automatically. - Routing of requested command to its associated execution function is done automatically. --------- Co-authored-by: devops <[email protected]> Co-authored-by: Tamás Tóth <[email protected]>
1 parent 22ac497 commit 8a39504

File tree

9 files changed

+883
-509
lines changed

9 files changed

+883
-509
lines changed
 

‎pyk/docs/conf.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
project = 'pyk'
1010
author = 'Runtime Verification, Inc'
1111
copyright = '2024, Runtime Verification, Inc'
12-
version = '0.1.687'
13-
release = '0.1.687'
12+
version = '0.1.688'
13+
release = '0.1.688'
1414

1515
# -- General configuration ---------------------------------------------------
1616
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

‎pyk/package/version

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.687
1+
0.1.688

‎pyk/pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "pyk"
7-
version = "0.1.687"
7+
version = "0.1.688"
88
description = ""
99
authors = [
1010
"Runtime Verification, Inc. <contact@runtimeverification.com>",

‎pyk/src/pyk/__main__.py

+29-322
Original file line numberDiff line numberDiff line change
@@ -1,345 +1,52 @@
11
from __future__ import annotations
22

3-
import json
43
import logging
54
import sys
6-
from argparse import ArgumentParser, FileType
7-
from enum import Enum
8-
from pathlib import Path
95
from typing import TYPE_CHECKING
106

11-
from graphviz import Digraph
12-
13-
from pyk.kast.inner import KInner
14-
from pyk.kore.rpc import ExecuteResult
15-
16-
from .cli.args import KCLIArgs
17-
from .cli.utils import LOG_FORMAT, dir_path, loglevel
18-
from .coverage import get_rule_by_id, strip_coverage_logger
19-
from .cterm import CTerm
20-
from .kast.manip import (
21-
flatten_label,
22-
minimize_rule,
23-
minimize_term,
24-
propagate_up_constraints,
25-
remove_source_map,
26-
split_config_and_constraints,
7+
from .cli.args import LoggingOptions
8+
from .cli.cli import CLI
9+
from .cli.pyk import (
10+
CoverageCommand,
11+
GraphImportsCommand,
12+
JsonToKoreCommand,
13+
KoreToJsonCommand,
14+
PrintCommand,
15+
ProveCommand,
16+
RPCKastCommand,
17+
RPCPrintCommand,
2718
)
28-
from .kast.outer import read_kast_definition
29-
from .kast.pretty import PrettyPrinter
30-
from .kore.parser import KoreParser
31-
from .kore.rpc import StopReason
32-
from .kore.syntax import Pattern, kore_term
33-
from .ktool.kprint import KPrint
34-
from .ktool.kprove import KProve
35-
from .prelude.k import GENERATED_TOP_CELL
36-
from .prelude.ml import is_top, mlAnd, mlOr
19+
from .cli.utils import LOG_FORMAT, loglevel
3720

3821
if TYPE_CHECKING:
39-
from argparse import Namespace
40-
from typing import Any, Final
22+
from typing import Final
4123

4224

4325
_LOGGER: Final = logging.getLogger(__name__)
4426

4527

46-
class PrintInput(Enum):
47-
KORE_JSON = 'kore-json'
48-
KAST_JSON = 'kast-json'
49-
50-
5128
def main() -> None:
5229
# KAST terms can end up nested quite deeply, because of the various assoc operators (eg. _Map_, _Set_, ...).
5330
# Most pyk operations are defined recursively, meaning you get a callstack the same depth as the term.
5431
# This change makes it so that in most cases, by default, pyk doesn't run out of stack space.
5532
sys.setrecursionlimit(10**7)
5633

57-
cli_parser = create_argument_parser()
58-
args = cli_parser.parse_args()
59-
60-
logging.basicConfig(level=loglevel(args), format=LOG_FORMAT)
61-
62-
executor_name = 'exec_' + args.command.lower().replace('-', '_')
63-
if executor_name not in globals():
64-
raise AssertionError(f'Unimplemented command: {args.command}')
65-
66-
execute = globals()[executor_name]
67-
execute(args)
68-
69-
70-
def exec_print(args: Namespace) -> None:
71-
kompiled_dir: Path = args.definition_dir
72-
printer = KPrint(kompiled_dir)
73-
if args.input == PrintInput.KORE_JSON:
74-
_LOGGER.info(f'Reading Kore JSON from file: {args.term.name}')
75-
kore = Pattern.from_json(args.term.read())
76-
term = printer.kore_to_kast(kore)
77-
else:
78-
_LOGGER.info(f'Reading Kast JSON from file: {args.term.name}')
79-
term = KInner.from_json(args.term.read())
80-
if is_top(term):
81-
args.output_file.write(printer.pretty_print(term))
82-
_LOGGER.info(f'Wrote file: {args.output_file.name}')
83-
else:
84-
if args.minimize:
85-
if args.omit_labels != '' and args.keep_cells != '':
86-
raise ValueError('You cannot use both --omit-labels and --keep-cells.')
87-
88-
abstract_labels = args.omit_labels.split(',') if args.omit_labels != '' else []
89-
keep_cells = args.keep_cells.split(',') if args.keep_cells != '' else []
90-
minimized_disjuncts = []
91-
92-
for disjunct in flatten_label('#Or', term):
93-
try:
94-
minimized = minimize_term(disjunct, abstract_labels=abstract_labels, keep_cells=keep_cells)
95-
config, constraint = split_config_and_constraints(minimized)
96-
except ValueError as err:
97-
raise ValueError('The minimized term does not contain a config cell.') from err
98-
99-
if not is_top(constraint):
100-
minimized_disjuncts.append(mlAnd([config, constraint], sort=GENERATED_TOP_CELL))
101-
else:
102-
minimized_disjuncts.append(config)
103-
term = propagate_up_constraints(mlOr(minimized_disjuncts, sort=GENERATED_TOP_CELL))
104-
105-
args.output_file.write(printer.pretty_print(term))
106-
_LOGGER.info(f'Wrote file: {args.output_file.name}')
107-
108-
109-
def exec_rpc_print(args: Namespace) -> None:
110-
kompiled_dir: Path = args.definition_dir
111-
printer = KPrint(kompiled_dir)
112-
input_dict = json.loads(args.input_file.read())
113-
output_buffer = []
114-
115-
def pretty_print_request(request_params: dict[str, Any]) -> list[str]:
116-
output_buffer = []
117-
non_state_keys = set(request_params.keys()).difference(['state'])
118-
for key in non_state_keys:
119-
output_buffer.append(f'{key}: {request_params[key]}')
120-
state = CTerm.from_kast(printer.kore_to_kast(kore_term(request_params['state'])))
121-
output_buffer.append('State:')
122-
output_buffer.append(printer.pretty_print(state.kast, sort_collections=True))
123-
return output_buffer
124-
125-
def pretty_print_execute_response(execute_result: ExecuteResult) -> list[str]:
126-
output_buffer = []
127-
output_buffer.append(f'Depth: {execute_result.depth}')
128-
output_buffer.append(f'Stop reason: {execute_result.reason.value}')
129-
if execute_result.reason == StopReason.TERMINAL_RULE or execute_result.reason == StopReason.CUT_POINT_RULE:
130-
output_buffer.append(f'Stop rule: {execute_result.rule}')
131-
output_buffer.append(
132-
f'Number of next states: {len(execute_result.next_states) if execute_result.next_states is not None else 0}'
133-
)
134-
state = CTerm.from_kast(printer.kore_to_kast(execute_result.state.kore))
135-
output_buffer.append('State:')
136-
output_buffer.append(printer.pretty_print(state.kast, sort_collections=True))
137-
if execute_result.next_states is not None:
138-
next_states = [CTerm.from_kast(printer.kore_to_kast(s.kore)) for s in execute_result.next_states]
139-
for i, s in enumerate(next_states):
140-
output_buffer.append(f'Next state #{i}:')
141-
output_buffer.append(printer.pretty_print(s.kast, sort_collections=True))
142-
return output_buffer
143-
144-
try:
145-
if 'method' in input_dict:
146-
output_buffer.append('JSON RPC request')
147-
output_buffer.append(f'id: {input_dict["id"]}')
148-
output_buffer.append(f'Method: {input_dict["method"]}')
149-
try:
150-
if 'state' in input_dict['params']:
151-
output_buffer += pretty_print_request(input_dict['params'])
152-
else: # this is an "add-module" request, skip trying to print state
153-
for key in input_dict['params'].keys():
154-
output_buffer.append(f'{key}: {input_dict["params"][key]}')
155-
except KeyError as e:
156-
_LOGGER.critical(f'Could not find key {str(e)} in input JSON file')
157-
exit(1)
158-
else:
159-
if not 'result' in input_dict:
160-
_LOGGER.critical('The input is neither a request not a resonse')
161-
exit(1)
162-
output_buffer.append('JSON RPC Response')
163-
output_buffer.append(f'id: {input_dict["id"]}')
164-
if list(input_dict['result'].keys()) == ['state']: # this is a "simplify" response
165-
output_buffer.append('Method: simplify')
166-
state = CTerm.from_kast(printer.kore_to_kast(kore_term(input_dict['result']['state'])))
167-
output_buffer.append('State:')
168-
output_buffer.append(printer.pretty_print(state.kast, sort_collections=True))
169-
elif list(input_dict['result'].keys()) == ['module']: # this is an "add-module" response
170-
output_buffer.append('Method: add-module')
171-
output_buffer.append('Module:')
172-
output_buffer.append(input_dict['result']['module'])
173-
else:
174-
try: # assume it is an "execute" response
175-
output_buffer.append('Method: execute')
176-
execute_result = ExecuteResult.from_dict(input_dict['result'])
177-
output_buffer += pretty_print_execute_response(execute_result)
178-
except KeyError as e:
179-
_LOGGER.critical(f'Could not find key {str(e)} in input JSON file')
180-
exit(1)
181-
if args.output_file is not None:
182-
args.output_file.write('\n'.join(output_buffer))
183-
else:
184-
print('\n'.join(output_buffer))
185-
except ValueError as e:
186-
# shorten and print the error message in case kore_to_kast throws ValueError
187-
_LOGGER.critical(str(e)[:200])
188-
exit(1)
189-
190-
191-
def exec_rpc_kast(args: Namespace) -> None:
192-
"""
193-
Convert an 'execute' JSON RPC response to a new 'execute' or 'simplify' request,
194-
copying parameters from a reference request.
195-
"""
196-
reference_request = json.loads(args.reference_request_file.read())
197-
input_dict = json.loads(args.response_file.read())
198-
execute_result = ExecuteResult.from_dict(input_dict['result'])
199-
non_state_keys = set(reference_request['params'].keys()).difference(['state'])
200-
request_params = {}
201-
for key in non_state_keys:
202-
request_params[key] = reference_request['params'][key]
203-
request_params['state'] = {'format': 'KORE', 'version': 1, 'term': execute_result.state.kore.dict}
204-
request = {
205-
'jsonrpc': reference_request['jsonrpc'],
206-
'id': reference_request['id'],
207-
'method': reference_request['method'],
208-
'params': request_params,
209-
}
210-
args.output_file.write(json.dumps(request))
211-
212-
213-
def exec_prove(args: Namespace) -> None:
214-
kompiled_dir: Path = args.definition_dir
215-
kprover = KProve(kompiled_dir, args.main_file)
216-
final_state = kprover.prove(Path(args.spec_file), spec_module_name=args.spec_module, args=args.kArgs)
217-
args.output_file.write(json.dumps(mlOr([state.kast for state in final_state]).to_dict()))
218-
_LOGGER.info(f'Wrote file: {args.output_file.name}')
219-
220-
221-
def exec_graph_imports(args: Namespace) -> None:
222-
kompiled_dir: Path = args.definition_dir
223-
kprinter = KPrint(kompiled_dir)
224-
definition = kprinter.definition
225-
import_graph = Digraph()
226-
graph_file = kompiled_dir / 'import-graph'
227-
for module in definition.modules:
228-
module_name = module.name
229-
import_graph.node(module_name)
230-
for module_import in module.imports:
231-
import_graph.edge(module_name, module_import.name)
232-
import_graph.render(graph_file)
233-
_LOGGER.info(f'Wrote file: {graph_file}')
234-
235-
236-
def exec_coverage(args: Namespace) -> None:
237-
kompiled_dir: Path = args.definition_dir
238-
definition = remove_source_map(read_kast_definition(kompiled_dir / 'compiled.json'))
239-
pretty_printer = PrettyPrinter(definition)
240-
for rid in args.coverage_file:
241-
rule = minimize_rule(strip_coverage_logger(get_rule_by_id(definition, rid.strip())))
242-
args.output.write('\n\n')
243-
args.output.write('Rule: ' + rid.strip())
244-
args.output.write('\nUnparsed:\n')
245-
args.output.write(pretty_printer.print(rule))
246-
_LOGGER.info(f'Wrote file: {args.output.name}')
247-
248-
249-
def exec_kore_to_json(args: Namespace) -> None:
250-
text = sys.stdin.read()
251-
kore = KoreParser(text).pattern()
252-
print(kore.json)
253-
254-
255-
def exec_json_to_kore(args: dict[str, Any]) -> None:
256-
text = sys.stdin.read()
257-
kore = Pattern.from_json(text)
258-
kore.write(sys.stdout)
259-
sys.stdout.write('\n')
260-
261-
262-
def create_argument_parser() -> ArgumentParser:
263-
k_cli_args = KCLIArgs()
264-
265-
definition_args = ArgumentParser(add_help=False)
266-
definition_args.add_argument('definition_dir', type=dir_path, help='Path to definition directory.')
267-
268-
pyk_args = ArgumentParser()
269-
pyk_args_command = pyk_args.add_subparsers(dest='command', required=True)
270-
271-
print_args = pyk_args_command.add_parser(
272-
'print',
273-
help='Pretty print a term.',
274-
parents=[k_cli_args.logging_args, definition_args, k_cli_args.display_args],
275-
)
276-
print_args.add_argument('term', type=FileType('r'), help='Input term (in format specified with --input).')
277-
print_args.add_argument('--input', default=PrintInput.KAST_JSON, type=PrintInput, choices=list(PrintInput))
278-
print_args.add_argument('--omit-labels', default='', nargs='?', help='List of labels to omit from output.')
279-
print_args.add_argument(
280-
'--keep-cells', default='', nargs='?', help='List of cells with primitive values to keep in output.'
281-
)
282-
print_args.add_argument('--output-file', type=FileType('w'), default='-')
283-
284-
rpc_print_args = pyk_args_command.add_parser(
285-
'rpc-print',
286-
help='Pretty-print an RPC request/response',
287-
parents=[k_cli_args.logging_args, definition_args],
288-
)
289-
rpc_print_args.add_argument(
290-
'input_file',
291-
type=FileType('r'),
292-
help='An input file containing the JSON RPC request or response with KoreJSON payload.',
293-
)
294-
rpc_print_args.add_argument('--output-file', type=FileType('w'), default='-')
295-
296-
rpc_kast_args = pyk_args_command.add_parser(
297-
'rpc-kast',
298-
help='Convert an "execute" JSON RPC response to a new "execute" or "simplify" request, copying parameters from a reference request.',
299-
parents=[k_cli_args.logging_args],
300-
)
301-
rpc_kast_args.add_argument(
302-
'reference_request_file',
303-
type=FileType('r'),
304-
help='An input file containing a JSON RPC request to server as a reference for the new request.',
305-
)
306-
rpc_kast_args.add_argument(
307-
'response_file',
308-
type=FileType('r'),
309-
help='An input file containing a JSON RPC response with KoreJSON payload.',
310-
)
311-
rpc_kast_args.add_argument('--output-file', type=FileType('w'), default='-')
312-
313-
prove_args = pyk_args_command.add_parser(
314-
'prove',
315-
help='Prove an input specification (using kprovex).',
316-
parents=[k_cli_args.logging_args, definition_args],
317-
)
318-
prove_args.add_argument('main_file', type=str, help='Main file used for kompilation.')
319-
prove_args.add_argument('spec_file', type=str, help='File with the specification module.')
320-
prove_args.add_argument('spec_module', type=str, help='Module with claims to be proven.')
321-
prove_args.add_argument('--output-file', type=FileType('w'), default='-')
322-
prove_args.add_argument('kArgs', nargs='*', help='Arguments to pass through to K invocation.')
323-
324-
pyk_args_command.add_parser(
325-
'graph-imports',
326-
help='Graph the imports of a given definition.',
327-
parents=[k_cli_args.logging_args, definition_args],
328-
)
329-
330-
coverage_args = pyk_args_command.add_parser(
331-
'coverage',
332-
help='Convert coverage file to human readable log.',
333-
parents=[k_cli_args.logging_args, definition_args],
334-
)
335-
coverage_args.add_argument('coverage_file', type=FileType('r'), help='Coverage file to build log for.')
336-
coverage_args.add_argument('-o', '--output', type=FileType('w'), default='-')
337-
338-
pyk_args_command.add_parser('kore-to-json', help='Convert textual KORE to JSON', parents=[k_cli_args.logging_args])
339-
340-
pyk_args_command.add_parser('json-to-kore', help='Convert JSON to textual KORE', parents=[k_cli_args.logging_args])
341-
342-
return pyk_args
34+
cli = CLI(
35+
[
36+
CoverageCommand,
37+
GraphImportsCommand,
38+
JsonToKoreCommand,
39+
KoreToJsonCommand,
40+
PrintCommand,
41+
ProveCommand,
42+
RPCKastCommand,
43+
RPCPrintCommand,
44+
]
45+
)
46+
command = cli.get_command()
47+
assert isinstance(command, LoggingOptions)
48+
logging.basicConfig(level=loglevel(command), format=LOG_FORMAT)
49+
command.exec()
34350

34451

34552
if __name__ == '__main__':

‎pyk/src/pyk/cli/args.py

+232-100
Original file line numberDiff line numberDiff line change
@@ -1,155 +1,287 @@
11
from __future__ import annotations
22

3-
from argparse import ArgumentParser
4-
from functools import cached_property
5-
from typing import TYPE_CHECKING
3+
import sys
4+
from argparse import FileType
5+
from typing import IO, TYPE_CHECKING, Any
66

7-
from ..utils import ensure_dir_path
7+
from pyk.utils import ensure_dir_path
8+
9+
from .cli import Options
810
from .utils import bug_report_arg, dir_path, file_path
911

1012
if TYPE_CHECKING:
13+
from argparse import ArgumentParser
14+
from pathlib import Path
1115
from typing import TypeVar
1216

17+
from ..utils import BugReport
18+
1319
T = TypeVar('T')
1420

1521

16-
class KCLIArgs:
17-
@cached_property
18-
def logging_args(self) -> ArgumentParser:
19-
args = ArgumentParser(add_help=False)
20-
args.add_argument('--verbose', '-v', default=False, action='store_true', help='Verbose output.')
21-
args.add_argument('--debug', default=False, action='store_true', help='Debug output.')
22-
return args
23-
24-
@cached_property
25-
def parallel_args(self) -> ArgumentParser:
26-
args = ArgumentParser(add_help=False)
27-
args.add_argument('--workers', '-j', default=1, type=int, help='Number of processes to run in parallel.')
28-
return args
29-
30-
@cached_property
31-
def bug_report_args(self) -> ArgumentParser:
32-
args = ArgumentParser(add_help=False)
33-
args.add_argument(
34-
'--bug-report',
35-
type=bug_report_arg,
36-
help='Generate bug report with given name',
22+
class LoggingOptions(Options):
23+
debug: bool
24+
verbose: bool
25+
26+
@staticmethod
27+
def default() -> dict[str, Any]:
28+
return {
29+
'verbose': False,
30+
'debug': False,
31+
}
32+
33+
@staticmethod
34+
def update_args(parser: ArgumentParser) -> None:
35+
parser.add_argument('--verbose', '-v', action='store_true', help='Verbose output.')
36+
parser.add_argument('--debug', action='store_true', help='Debug output.')
37+
38+
39+
class OutputFileOptions(Options):
40+
output_file: IO[Any]
41+
42+
@staticmethod
43+
def default() -> dict[str, Any]:
44+
return {
45+
'output_file': sys.stdout,
46+
}
47+
48+
@staticmethod
49+
def update_args(parser: ArgumentParser) -> None:
50+
parser.add_argument('--output-file', type=FileType('w'))
51+
52+
53+
class DefinitionOptions(Options):
54+
definition_dir: Path
55+
56+
@staticmethod
57+
def update_args(parser: ArgumentParser) -> None:
58+
parser.add_argument('definition_dir', type=dir_path, help='Path to definition directory.')
59+
60+
61+
class DisplayOptions(Options):
62+
minimize: bool
63+
64+
@staticmethod
65+
def default() -> dict[str, Any]:
66+
return {
67+
'minimize': True,
68+
}
69+
70+
@staticmethod
71+
def update_args(parser: ArgumentParser) -> None:
72+
parser.add_argument('--minimize', dest='minimize', action='store_true', help='Minimize output.')
73+
parser.add_argument('--no-minimize', dest='minimize', action='store_false', help='Do not minimize output.')
74+
75+
76+
class KDefinitionOptions(Options):
77+
includes: list[str]
78+
main_module: str | None
79+
syntax_module: str | None
80+
spec_module: str | None
81+
definition_dir: Path | None
82+
md_selector: str
83+
84+
@staticmethod
85+
def default() -> dict[str, Any]:
86+
return {
87+
'spec_module': None,
88+
'main_module': None,
89+
'syntax_module': None,
90+
'definition_dir': None,
91+
'md_selector': 'k',
92+
'includes': [],
93+
}
94+
95+
@staticmethod
96+
def update_args(parser: ArgumentParser) -> None:
97+
parser.add_argument(
98+
'-I', type=str, dest='includes', action='append', help='Directories to lookup K definitions in.'
99+
)
100+
parser.add_argument('--main-module', type=str, help='Name of the main module.')
101+
parser.add_argument('--syntax-module', type=str, help='Name of the syntax module.')
102+
parser.add_argument('--spec-module', type=str, help='Name of the spec module.')
103+
parser.add_argument('--definition', type=dir_path, dest='definition_dir', help='Path to definition to use.')
104+
parser.add_argument(
105+
'--md-selector',
106+
type=str,
107+
help='Code selector expression to use when reading markdown.',
108+
)
109+
110+
111+
class SaveDirOptions(Options):
112+
save_directory: Path | None
113+
114+
@staticmethod
115+
def default() -> dict[str, Any]:
116+
return {
117+
'save_directory': None,
118+
}
119+
120+
@staticmethod
121+
def update_args(parser: ArgumentParser) -> None:
122+
parser.add_argument('--save-directory', type=ensure_dir_path, help='Path to where CFGs are stored.')
123+
124+
125+
class SpecOptions(SaveDirOptions):
126+
spec_file: Path
127+
claim_labels: list[str] | None
128+
exclude_claim_labels: list[str]
129+
130+
@staticmethod
131+
def default() -> dict[str, Any]:
132+
return {
133+
'claim_labels': None,
134+
'exclude_claim_labels': [],
135+
}
136+
137+
@staticmethod
138+
def update_args(parser: ArgumentParser) -> None:
139+
parser.add_argument('spec_file', type=file_path, help='Path to spec file.')
140+
parser.add_argument(
141+
'--claim',
142+
type=str,
143+
dest='claim_labels',
144+
action='append',
145+
help='Only prove listed claims, MODULE_NAME.claim-id',
146+
)
147+
parser.add_argument(
148+
'--exclude-claim',
149+
type=str,
150+
dest='exclude_claim_labels',
151+
action='append',
152+
help='Skip listed claims, MODULE_NAME.claim-id',
37153
)
38-
return args
39154

40-
@cached_property
41-
def kompile_args(self) -> ArgumentParser:
42-
args = ArgumentParser(add_help=False)
43-
args.add_argument(
155+
156+
class KompileOptions(Options):
157+
emit_json: bool
158+
ccopts: list[str]
159+
llvm_kompile: bool
160+
llvm_library: bool
161+
enable_llvm_debug: bool
162+
read_only: bool
163+
o0: bool
164+
o1: bool
165+
o2: bool
166+
o3: bool
167+
168+
@staticmethod
169+
def default() -> dict[str, Any]:
170+
return {
171+
'emit_json': True,
172+
'llvm_kompile': False,
173+
'llvm_library': False,
174+
'enable_llvm_debug': False,
175+
'read_only': False,
176+
'o0': False,
177+
'o1': False,
178+
'o2': False,
179+
'o3': False,
180+
'ccopts': [],
181+
}
182+
183+
@staticmethod
184+
def update_args(parser: ArgumentParser) -> None:
185+
parser.add_argument(
44186
'--emit-json',
45187
dest='emit_json',
46-
default=True,
47188
action='store_true',
48189
help='Emit JSON definition after compilation.',
49190
)
50-
args.add_argument(
191+
parser.add_argument(
51192
'--no-emit-json', dest='emit_json', action='store_false', help='Do not JSON definition after compilation.'
52193
)
53-
args.add_argument(
194+
parser.add_argument(
54195
'-ccopt',
55196
dest='ccopts',
56-
default=[],
57197
action='append',
58198
help='Additional arguments to pass to llvm-kompile.',
59199
)
60-
args.add_argument(
200+
parser.add_argument(
61201
'--no-llvm-kompile',
62202
dest='llvm_kompile',
63-
default=True,
64203
action='store_false',
65204
help='Do not run llvm-kompile process.',
66205
)
67-
args.add_argument(
206+
parser.add_argument(
68207
'--with-llvm-library',
69208
dest='llvm_library',
70-
default=False,
71209
action='store_true',
72210
help='Make kompile generate a dynamic llvm library.',
73211
)
74-
args.add_argument(
212+
parser.add_argument(
75213
'--enable-llvm-debug',
76214
dest='enable_llvm_debug',
77-
default=False,
78215
action='store_true',
79216
help='Make kompile generate debug symbols for llvm.',
80217
)
81-
args.add_argument(
218+
parser.add_argument(
82219
'--read-only-kompiled-directory',
83220
dest='read_only',
84-
default=False,
85221
action='store_true',
86222
help='Generated a kompiled directory that K will not attempt to write to afterwards.',
87223
)
88-
args.add_argument('-O0', dest='o0', default=False, action='store_true', help='Optimization level 0.')
89-
args.add_argument('-O1', dest='o1', default=False, action='store_true', help='Optimization level 1.')
90-
args.add_argument('-O2', dest='o2', default=False, action='store_true', help='Optimization level 2.')
91-
args.add_argument('-O3', dest='o3', default=False, action='store_true', help='Optimization level 3.')
92-
return args
93-
94-
@cached_property
95-
def smt_args(self) -> ArgumentParser:
96-
args = ArgumentParser(add_help=False)
97-
args.add_argument('--smt-timeout', dest='smt_timeout', type=int, help='Timeout in ms to use for SMT queries.')
98-
args.add_argument(
224+
parser.add_argument('-O0', dest='o0', action='store_true', help='Optimization level 0.')
225+
parser.add_argument('-O1', dest='o1', action='store_true', help='Optimization level 1.')
226+
parser.add_argument('-O2', dest='o2', action='store_true', help='Optimization level 2.')
227+
parser.add_argument('-O3', dest='o3', action='store_true', help='Optimization level 3.')
228+
229+
230+
class ParallelOptions(Options):
231+
workers: int
232+
233+
@staticmethod
234+
def default() -> dict[str, Any]:
235+
return {
236+
'workers': 1,
237+
}
238+
239+
@staticmethod
240+
def update_args(parser: ArgumentParser) -> None:
241+
parser.add_argument('--workers', '-j', type=int, help='Number of processes to run in parallel.')
242+
243+
244+
class BugReportOptions(Options):
245+
bug_report: BugReport | None
246+
247+
@staticmethod
248+
def default() -> dict[str, Any]:
249+
return {'bug_report': None}
250+
251+
@staticmethod
252+
def update_args(parser: ArgumentParser) -> None:
253+
parser.add_argument(
254+
'--bug-report',
255+
type=bug_report_arg,
256+
help='Generate bug report with given name',
257+
)
258+
259+
260+
class SMTOptions(Options):
261+
smt_timeout: int
262+
smt_retry_limit: int
263+
smt_tactic: str | None
264+
265+
@staticmethod
266+
def default() -> dict[str, Any]:
267+
return {
268+
'smt_timeout': 300,
269+
'smt_retry_limit': 10,
270+
'smt_tactic': None,
271+
}
272+
273+
@staticmethod
274+
def update_args(parser: ArgumentParser) -> None:
275+
parser.add_argument('--smt-timeout', dest='smt_timeout', type=int, help='Timeout in ms to use for SMT queries.')
276+
parser.add_argument(
99277
'--smt-retry-limit',
100278
dest='smt_retry_limit',
101279
type=int,
102280
help='Number of times to retry SMT queries with scaling timeouts.',
103281
)
104-
args.add_argument(
282+
parser.add_argument(
105283
'--smt-tactic',
106284
dest='smt_tactic',
107285
type=str,
108286
help='Z3 tactic to use when checking satisfiability. Example: (check-sat-using smt)',
109287
)
110-
return args
111-
112-
@cached_property
113-
def display_args(self) -> ArgumentParser:
114-
args = ArgumentParser(add_help=False)
115-
args.add_argument('--minimize', dest='minimize', default=True, action='store_true', help='Minimize output.')
116-
args.add_argument('--no-minimize', dest='minimize', action='store_false', help='Do not minimize output.')
117-
return args
118-
119-
@cached_property
120-
def definition_args(self) -> ArgumentParser:
121-
args = ArgumentParser(add_help=False)
122-
args.add_argument(
123-
'-I', type=str, dest='includes', default=[], action='append', help='Directories to lookup K definitions in.'
124-
)
125-
args.add_argument('--main-module', default=None, type=str, help='Name of the main module.')
126-
args.add_argument('--syntax-module', default=None, type=str, help='Name of the syntax module.')
127-
args.add_argument('--spec-module', default=None, type=str, help='Name of the spec module.')
128-
args.add_argument('--definition', type=dir_path, dest='definition_dir', help='Path to definition to use.')
129-
args.add_argument(
130-
'--md-selector',
131-
type=str,
132-
help='Code selector expression to use when reading markdown.',
133-
)
134-
return args
135-
136-
@cached_property
137-
def spec_args(self) -> ArgumentParser:
138-
args = ArgumentParser(add_help=False)
139-
args.add_argument('spec_file', type=file_path, help='Path to spec file.')
140-
args.add_argument('--save-directory', type=ensure_dir_path, help='Path to where CFGs are stored.')
141-
args.add_argument(
142-
'--claim',
143-
type=str,
144-
dest='claim_labels',
145-
action='append',
146-
help='Only prove listed claims, MODULE_NAME.claim-id',
147-
)
148-
args.add_argument(
149-
'--exclude-claim',
150-
type=str,
151-
dest='exclude_claim_labels',
152-
action='append',
153-
help='Skip listed claims, MODULE_NAME.claim-id',
154-
)
155-
return args

‎pyk/src/pyk/cli/cli.py

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from __future__ import annotations
2+
3+
from abc import ABC, abstractmethod
4+
from argparse import ArgumentParser
5+
from collections.abc import Iterable
6+
from typing import TYPE_CHECKING, Any
7+
8+
if TYPE_CHECKING:
9+
from argparse import _SubParsersAction
10+
11+
12+
class CLI:
13+
commands: list[type[Command]]
14+
top_level_args: Iterable[type[Options]]
15+
16+
# Input a list of all Command types to be used
17+
def __init__(self, commands: Iterable[type[Command]], top_level_args: Iterable[type[Options]] = ()):
18+
self.commands = list(commands)
19+
self.top_level_args = top_level_args
20+
21+
# Return an instance of the correct Options subclass by matching its name with the requested command
22+
def generate_command(self, args: dict[str, Any]) -> Command:
23+
command = args['command'].lower()
24+
for cmd_type in self.commands:
25+
if cmd_type.name() == command:
26+
if issubclass(cmd_type, Options):
27+
return cmd_type(args)
28+
else:
29+
return cmd_type()
30+
raise ValueError(f'Unrecognized command: {command}')
31+
32+
# Generate the parsers for all commands
33+
def add_parsers(self, base: _SubParsersAction) -> _SubParsersAction:
34+
for cmd_type in self.commands:
35+
base = cmd_type.parser(base)
36+
return base
37+
38+
def create_argument_parser(self) -> ArgumentParser:
39+
pyk_args = ArgumentParser(parents=[tla.all_args() for tla in self.top_level_args])
40+
pyk_args_command = pyk_args.add_subparsers(dest='command', required=True)
41+
42+
pyk_args_command = self.add_parsers(pyk_args_command)
43+
44+
return pyk_args
45+
46+
def get_command(self) -> Command:
47+
parser = self.create_argument_parser()
48+
args = parser.parse_args()
49+
stripped_args = {
50+
key: val
51+
for (key, val) in vars(args).items()
52+
if val is not None and not (isinstance(val, Iterable) and not val)
53+
}
54+
return self.generate_command(stripped_args)
55+
56+
def get_and_exec_command(self) -> None:
57+
cmd = self.get_command()
58+
cmd.exec()
59+
60+
61+
class Command(ABC):
62+
@staticmethod
63+
@abstractmethod
64+
def name() -> str:
65+
...
66+
67+
@staticmethod
68+
@abstractmethod
69+
def help_str() -> str:
70+
...
71+
72+
@abstractmethod
73+
def exec(self) -> None:
74+
...
75+
76+
@classmethod
77+
def parser(cls, base: _SubParsersAction) -> _SubParsersAction:
78+
all_args = [cls.all_args()] if issubclass(cls, Options) else []
79+
base.add_parser(
80+
name=cls.name(),
81+
help=cls.help_str(),
82+
parents=all_args,
83+
)
84+
return base
85+
86+
87+
class Options:
88+
def __init__(self, args: dict[str, Any]) -> None:
89+
# Get defaults from this and all superclasses that define them, preferring the most specific class
90+
defaults: dict[str, Any] = {}
91+
for cl in reversed(type(self).mro()):
92+
if hasattr(cl, 'default'):
93+
defaults = defaults | cl.default()
94+
95+
# Overwrite defaults with args from command line
96+
_args = defaults | args
97+
98+
for attr, val in _args.items():
99+
self.__setattr__(attr, val)
100+
101+
@classmethod
102+
def all_args(cls: type[Options]) -> ArgumentParser:
103+
# Collect args from this and all superclasses
104+
parser = ArgumentParser(add_help=False)
105+
mro = cls.mro()
106+
mro.reverse()
107+
for cl in mro:
108+
if hasattr(cl, 'update_args') and 'update_args' in cl.__dict__:
109+
cl.update_args(parser)
110+
return parser

‎pyk/src/pyk/cli/pyk.py

+381
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import sys
5+
from argparse import FileType
6+
from enum import Enum
7+
from pathlib import Path
8+
from typing import IO, TYPE_CHECKING, Any
9+
10+
from graphviz import Digraph
11+
12+
from pyk.coverage import get_rule_by_id, strip_coverage_logger
13+
from pyk.cterm import CTerm
14+
from pyk.kast.inner import KInner
15+
from pyk.kast.manip import (
16+
flatten_label,
17+
minimize_rule,
18+
minimize_term,
19+
propagate_up_constraints,
20+
remove_source_map,
21+
split_config_and_constraints,
22+
)
23+
from pyk.kast.outer import read_kast_definition
24+
from pyk.kast.pretty import PrettyPrinter
25+
from pyk.kore.parser import KoreParser
26+
from pyk.kore.rpc import ExecuteResult, StopReason
27+
from pyk.kore.syntax import Pattern, kore_term
28+
from pyk.ktool.kprint import KPrint
29+
from pyk.ktool.kprove import KProve
30+
from pyk.prelude.k import GENERATED_TOP_CELL
31+
from pyk.prelude.ml import is_top, mlAnd, mlOr
32+
from pyk.utils import _LOGGER
33+
34+
from .args import DefinitionOptions, DisplayOptions, LoggingOptions, OutputFileOptions
35+
from .cli import Command
36+
37+
if TYPE_CHECKING:
38+
from argparse import ArgumentParser
39+
from collections.abc import Iterable
40+
41+
42+
class PrintInput(Enum):
43+
KORE_JSON = 'kore-json'
44+
KAST_JSON = 'kast-json'
45+
46+
47+
class JsonToKoreCommand(Command, LoggingOptions):
48+
@staticmethod
49+
def name() -> str:
50+
return 'json-to-kore'
51+
52+
@staticmethod
53+
def help_str() -> str:
54+
return 'Convert JSON to textual KORE'
55+
56+
def exec(self) -> None:
57+
text = sys.stdin.read()
58+
kore = Pattern.from_json(text)
59+
kore.write(sys.stdout)
60+
sys.stdout.write('\n')
61+
62+
63+
class KoreToJsonCommand(Command, LoggingOptions):
64+
@staticmethod
65+
def name() -> str:
66+
return 'kore-to-json'
67+
68+
@staticmethod
69+
def help_str() -> str:
70+
return 'Convert textual KORE to JSON'
71+
72+
def exec(self) -> None:
73+
text = sys.stdin.read()
74+
kore = KoreParser(text).pattern()
75+
print(kore.json)
76+
77+
78+
class CoverageCommand(Command, DefinitionOptions, OutputFileOptions, LoggingOptions):
79+
coverage_file: IO[Any]
80+
81+
@staticmethod
82+
def name() -> str:
83+
return 'coverage'
84+
85+
@staticmethod
86+
def help_str() -> str:
87+
return 'Convert coverage file to human readable log.'
88+
89+
@staticmethod
90+
def update_args(parser: ArgumentParser) -> None:
91+
parser.add_argument('coverage_file', type=FileType('r'), help='Coverage file to build log for.')
92+
93+
def exec(self) -> None:
94+
kompiled_dir: Path = self.definition_dir
95+
definition = remove_source_map(read_kast_definition(kompiled_dir / 'compiled.json'))
96+
pretty_printer = PrettyPrinter(definition)
97+
for rid in self.coverage_file:
98+
rule = minimize_rule(strip_coverage_logger(get_rule_by_id(definition, rid.strip())))
99+
self.output_file.write('\n\n')
100+
self.output_file.write('Rule: ' + rid.strip())
101+
self.output_file.write('\nUnparsed:\n')
102+
self.output_file.write(pretty_printer.print(rule))
103+
_LOGGER.info(f'Wrote file: {self.output_file.name}')
104+
105+
106+
class GraphImportsCommand(Command, DefinitionOptions, LoggingOptions):
107+
@staticmethod
108+
def name() -> str:
109+
return 'graph-imports'
110+
111+
@staticmethod
112+
def help_str() -> str:
113+
return 'Graph the imports of a given definition.'
114+
115+
def exec(self) -> None:
116+
kompiled_dir: Path = self.definition_dir
117+
kprinter = KPrint(kompiled_dir)
118+
definition = kprinter.definition
119+
import_graph = Digraph()
120+
graph_file = kompiled_dir / 'import-graph'
121+
for module in definition.modules:
122+
module_name = module.name
123+
import_graph.node(module_name)
124+
for module_import in module.imports:
125+
import_graph.edge(module_name, module_import.name)
126+
import_graph.render(graph_file)
127+
_LOGGER.info(f'Wrote file: {graph_file}')
128+
129+
130+
class RPCKastCommand(Command, OutputFileOptions, LoggingOptions):
131+
reference_request_file: IO[Any]
132+
response_file: IO[Any]
133+
134+
@staticmethod
135+
def name() -> str:
136+
return 'rpc-kast'
137+
138+
@staticmethod
139+
def help_str() -> str:
140+
return 'Convert an "execute" JSON RPC response to a new "execute" or "simplify" request, copying parameters from a reference request.'
141+
142+
@staticmethod
143+
def update_args(parser: ArgumentParser) -> None:
144+
parser.add_argument(
145+
'reference_request_file',
146+
type=FileType('r'),
147+
help='An input file containing a JSON RPC request to server as a reference for the new request.',
148+
)
149+
parser.add_argument(
150+
'response_file',
151+
type=FileType('r'),
152+
help='An input file containing a JSON RPC response with KoreJSON payload.',
153+
)
154+
155+
def exec(self) -> None:
156+
"""
157+
Convert an 'execute' JSON RPC response to a new 'execute' or 'simplify' request,
158+
copying parameters from a reference request.
159+
"""
160+
reference_request = json.loads(self.reference_request_file.read())
161+
input_dict = json.loads(self.response_file.read())
162+
execute_result = ExecuteResult.from_dict(input_dict['result'])
163+
non_state_keys = set(reference_request['params'].keys()).difference(['state'])
164+
request_params = {}
165+
for key in non_state_keys:
166+
request_params[key] = reference_request['params'][key]
167+
request_params['state'] = {'format': 'KORE', 'version': 1, 'term': execute_result.state.kore.dict}
168+
request = {
169+
'jsonrpc': reference_request['jsonrpc'],
170+
'id': reference_request['id'],
171+
'method': reference_request['method'],
172+
'params': request_params,
173+
}
174+
self.output_file.write(json.dumps(request))
175+
176+
177+
class RPCPrintCommand(Command, DefinitionOptions, OutputFileOptions, LoggingOptions):
178+
input_file: IO[Any]
179+
180+
@staticmethod
181+
def name() -> str:
182+
return 'rpc-print'
183+
184+
@staticmethod
185+
def help_str() -> str:
186+
return 'Pretty-print an RPC request/response'
187+
188+
@staticmethod
189+
def update_args(parser: ArgumentParser) -> None:
190+
parser.add_argument(
191+
'input_file',
192+
type=FileType('r'),
193+
help='An input file containing the JSON RPC request or response with KoreJSON payload.',
194+
)
195+
196+
def exec(self) -> None:
197+
kompiled_dir: Path = self.definition_dir
198+
printer = KPrint(kompiled_dir)
199+
input_dict = json.loads(self.input_file.read())
200+
output_buffer = []
201+
202+
def pretty_print_request(request_params: dict[str, Any]) -> list[str]:
203+
output_buffer = []
204+
non_state_keys = set(request_params.keys()).difference(['state'])
205+
for key in non_state_keys:
206+
output_buffer.append(f'{key}: {request_params[key]}')
207+
state = CTerm.from_kast(printer.kore_to_kast(kore_term(request_params['state'])))
208+
output_buffer.append('State:')
209+
output_buffer.append(printer.pretty_print(state.kast, sort_collections=True))
210+
return output_buffer
211+
212+
def pretty_print_execute_response(execute_result: ExecuteResult) -> list[str]:
213+
output_buffer = []
214+
output_buffer.append(f'Depth: {execute_result.depth}')
215+
output_buffer.append(f'Stop reason: {execute_result.reason.value}')
216+
if execute_result.reason == StopReason.TERMINAL_RULE or execute_result.reason == StopReason.CUT_POINT_RULE:
217+
output_buffer.append(f'Stop rule: {execute_result.rule}')
218+
output_buffer.append(
219+
f'Number of next states: {len(execute_result.next_states) if execute_result.next_states is not None else 0}'
220+
)
221+
state = CTerm.from_kast(printer.kore_to_kast(execute_result.state.kore))
222+
output_buffer.append('State:')
223+
output_buffer.append(printer.pretty_print(state.kast, sort_collections=True))
224+
if execute_result.next_states is not None:
225+
next_states = [CTerm.from_kast(printer.kore_to_kast(s.kore)) for s in execute_result.next_states]
226+
for i, s in enumerate(next_states):
227+
output_buffer.append(f'Next state #{i}:')
228+
output_buffer.append(printer.pretty_print(s.kast, sort_collections=True))
229+
return output_buffer
230+
231+
try:
232+
if 'method' in input_dict:
233+
output_buffer.append('JSON RPC request')
234+
output_buffer.append(f'id: {input_dict["id"]}')
235+
output_buffer.append(f'Method: {input_dict["method"]}')
236+
try:
237+
if 'state' in input_dict['params']:
238+
output_buffer += pretty_print_request(input_dict['params'])
239+
else: # this is an "add-module" request, skip trying to print state
240+
for key in input_dict['params'].keys():
241+
output_buffer.append(f'{key}: {input_dict["params"][key]}')
242+
except KeyError as e:
243+
_LOGGER.critical(f'Could not find key {str(e)} in input JSON file')
244+
exit(1)
245+
else:
246+
if not 'result' in input_dict:
247+
_LOGGER.critical('The input is neither a request not a resonse')
248+
exit(1)
249+
output_buffer.append('JSON RPC Response')
250+
output_buffer.append(f'id: {input_dict["id"]}')
251+
if list(input_dict['result'].keys()) == ['state']: # this is a "simplify" response
252+
output_buffer.append('Method: simplify')
253+
state = CTerm.from_kast(printer.kore_to_kast(kore_term(input_dict['result']['state'])))
254+
output_buffer.append('State:')
255+
output_buffer.append(printer.pretty_print(state.kast, sort_collections=True))
256+
elif list(input_dict['result'].keys()) == ['module']: # this is an "add-module" response
257+
output_buffer.append('Method: add-module')
258+
output_buffer.append('Module:')
259+
output_buffer.append(input_dict['result']['module'])
260+
else:
261+
try: # assume it is an "execute" response
262+
output_buffer.append('Method: execute')
263+
execute_result = ExecuteResult.from_dict(input_dict['result'])
264+
output_buffer += pretty_print_execute_response(execute_result)
265+
except KeyError as e:
266+
_LOGGER.critical(f'Could not find key {str(e)} in input JSON file')
267+
exit(1)
268+
if self.output_file is not None:
269+
self.output_file.write('\n'.join(output_buffer))
270+
else:
271+
print('\n'.join(output_buffer))
272+
except ValueError as e:
273+
# shorten and print the error message in case kore_to_kast throws ValueError
274+
_LOGGER.critical(str(e)[:200])
275+
exit(1)
276+
277+
278+
class PrintCommand(Command, DefinitionOptions, OutputFileOptions, DisplayOptions, LoggingOptions):
279+
term: IO[Any]
280+
input: PrintInput
281+
minimize: bool
282+
omit_labels: str | None
283+
keep_cells: str | None
284+
285+
@staticmethod
286+
def name() -> str:
287+
return 'print'
288+
289+
@staticmethod
290+
def help_str() -> str:
291+
return 'Pretty print a term.'
292+
293+
@staticmethod
294+
def default() -> dict[str, Any]:
295+
return {
296+
'input': PrintInput.KAST_JSON,
297+
'omit_labels': None,
298+
'keep_cells': None,
299+
}
300+
301+
@staticmethod
302+
def update_args(parser: ArgumentParser) -> None:
303+
parser.add_argument(
304+
'term', type=FileType('r'), help='File containing input term (in format specified with --input).'
305+
)
306+
parser.add_argument('--input', type=PrintInput, choices=list(PrintInput))
307+
parser.add_argument('--omit-labels', nargs='?', help='List of labels to omit from output.')
308+
parser.add_argument('--keep-cells', nargs='?', help='List of cells with primitive values to keep in output.')
309+
310+
def exec(self) -> None:
311+
kompiled_dir: Path = self.definition_dir
312+
printer = KPrint(kompiled_dir)
313+
if self.input == PrintInput.KORE_JSON:
314+
_LOGGER.info(f'Reading Kore JSON from file: {self.term.name}')
315+
kore = Pattern.from_json(self.term.read())
316+
term = printer.kore_to_kast(kore)
317+
else:
318+
_LOGGER.info(f'Reading Kast JSON from file: {self.term.name}')
319+
term = KInner.from_json(self.term.read())
320+
if is_top(term):
321+
self.output_file.write(printer.pretty_print(term))
322+
_LOGGER.info(f'Wrote file: {self.output_file.name}')
323+
else:
324+
if self.minimize:
325+
if self.omit_labels != None and self.keep_cells != None:
326+
raise ValueError('You cannot use both --omit-labels and --keep-cells.')
327+
328+
abstract_labels = self.omit_labels.split(',') if self.omit_labels is not None else []
329+
keep_cells = self.keep_cells.split(',') if self.keep_cells is not None else []
330+
minimized_disjuncts = []
331+
332+
for disjunct in flatten_label('#Or', term):
333+
try:
334+
minimized = minimize_term(disjunct, abstract_labels=abstract_labels, keep_cells=keep_cells)
335+
config, constraint = split_config_and_constraints(minimized)
336+
except ValueError as err:
337+
raise ValueError('The minimized term does not contain a config cell.') from err
338+
339+
if not is_top(constraint):
340+
minimized_disjuncts.append(mlAnd([config, constraint], sort=GENERATED_TOP_CELL))
341+
else:
342+
minimized_disjuncts.append(config)
343+
term = propagate_up_constraints(mlOr(minimized_disjuncts, sort=GENERATED_TOP_CELL))
344+
345+
self.output_file.write(printer.pretty_print(term))
346+
_LOGGER.info(f'Wrote file: {self.output_file.name}')
347+
348+
349+
class ProveCommand(Command, DefinitionOptions, OutputFileOptions, LoggingOptions):
350+
main_file: Path
351+
spec_file: Path
352+
spec_module: str
353+
k_args: Iterable[str]
354+
355+
@staticmethod
356+
def name() -> str:
357+
return 'prove'
358+
359+
@staticmethod
360+
def help_str() -> str:
361+
return 'Prove an input specification (using kprovex).'
362+
363+
@staticmethod
364+
def default() -> dict[str, Any]:
365+
return {
366+
'k_args': [],
367+
}
368+
369+
@staticmethod
370+
def update_args(parser: ArgumentParser) -> None:
371+
parser.add_argument('main_file', type=str, help='Main file used for kompilation.')
372+
parser.add_argument('spec_file', type=str, help='File with the specification module.')
373+
parser.add_argument('spec_module', type=str, help='Module with claims to be proven.')
374+
parser.add_argument('k_args', nargs='*', help='Arguments to pass through to K invocation.')
375+
376+
def exec(self) -> None:
377+
kompiled_dir: Path = self.definition_dir
378+
kprover = KProve(kompiled_dir, self.main_file)
379+
final_state = kprover.prove(Path(self.spec_file), spec_module_name=self.spec_module, args=self.k_args)
380+
self.output_file.write(json.dumps(mlOr([state.kast for state in final_state]).to_dict()))
381+
_LOGGER.info(f'Wrote file: {self.output_file.name}')

‎pyk/src/pyk/cli/utils.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@
1212
from typing import Final, TypeVar
1313

1414
from ..kcfg.kcfg import NodeIdLike
15+
from .args import LoggingOptions
1516

1617
T1 = TypeVar('T1')
1718
T2 = TypeVar('T2')
1819

1920
LOG_FORMAT: Final = '%(levelname)s %(asctime)s %(name)s - %(message)s'
2021

2122

22-
def loglevel(args: Namespace) -> int:
23+
def loglevel(args: LoggingOptions | Namespace) -> int:
2324
if args.debug:
2425
return logging.DEBUG
2526

‎pyk/src/pyk/kdist/__main__.py

+125-82
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22

33
import fnmatch
44
import logging
5-
from argparse import ArgumentParser
6-
from typing import TYPE_CHECKING
5+
from typing import TYPE_CHECKING, Any
76

8-
from pyk.cli.args import KCLIArgs
7+
from pyk.cli.args import LoggingOptions
8+
from pyk.cli.cli import CLI, Command
99
from pyk.cli.utils import loglevel
1010

1111
from ..kdist import kdist, target_ids
1212

1313
if TYPE_CHECKING:
14-
from argparse import Namespace
14+
from argparse import ArgumentParser
1515
from typing import Final
1616

1717

@@ -20,42 +20,14 @@
2020

2121

2222
def main() -> None:
23-
args = _parse_arguments()
24-
25-
logging.basicConfig(level=loglevel(args), format=_LOG_FORMAT)
26-
27-
if args.command == 'build':
28-
_exec_build(**vars(args))
29-
30-
elif args.command == 'clean':
31-
_exec_clean(args.target)
32-
33-
elif args.command == 'which':
34-
_exec_which(args.target)
35-
36-
elif args.command == 'list':
37-
_exec_list()
38-
39-
else:
40-
raise AssertionError()
41-
42-
43-
def _exec_build(
44-
command: str,
45-
targets: list[str],
46-
args: list[str],
47-
jobs: int,
48-
force: bool,
49-
verbose: bool,
50-
debug: bool,
51-
) -> None:
52-
kdist.build(
53-
target_ids=_process_targets(targets),
54-
args=_process_args(args),
55-
jobs=jobs,
56-
force=force,
57-
verbose=verbose or debug,
23+
kdist_cli = CLI(
24+
{KDistBuildCommand, KDistCleanCommand, KDistWhichCommand, KDistListCommand}, top_level_args=[LoggingOptions]
5825
)
26+
cmd = kdist_cli.get_command()
27+
assert isinstance(cmd, LoggingOptions)
28+
print(vars(cmd))
29+
logging.basicConfig(level=loglevel(cmd), format=_LOG_FORMAT)
30+
cmd.exec()
5931

6032

6133
def _process_targets(targets: list[str]) -> list[str]:
@@ -80,66 +52,137 @@ def _process_args(args: list[str]) -> dict[str, str]:
8052
return res
8153

8254

83-
def _exec_clean(target: str | None) -> None:
84-
res = kdist.clean(target)
85-
print(res)
55+
class KDistBuildCommand(Command, LoggingOptions):
56+
targets: list[str]
57+
force: bool
58+
jobs: int
59+
args: list[str]
60+
61+
@staticmethod
62+
def name() -> str:
63+
return 'build'
64+
65+
@staticmethod
66+
def help_str() -> str:
67+
return 'build targets'
68+
69+
@staticmethod
70+
def default() -> dict[str, Any]:
71+
return {
72+
'force': False,
73+
'jobs': 1,
74+
'targets': ['*'],
75+
'args': [],
76+
}
77+
78+
@staticmethod
79+
def update_args(parser: ArgumentParser) -> None:
80+
parser.add_argument('targets', metavar='TARGET', nargs='*', help='target to build')
81+
parser.add_argument(
82+
'-a',
83+
'--arg',
84+
dest='args',
85+
metavar='ARG',
86+
action='append',
87+
help='build with argument',
88+
)
89+
parser.add_argument('-f', '--force', action='store_true', help='force build')
90+
parser.add_argument('-j', '--jobs', metavar='N', type=int, help='maximal number of build jobs')
91+
92+
def exec(self) -> None:
93+
print(self.verbose)
94+
print(self.debug)
95+
kdist.build(
96+
target_ids=_process_targets(self.targets),
97+
args=_process_args(self.args),
98+
jobs=self.jobs,
99+
force=self.force,
100+
verbose=self.verbose or self.debug,
101+
)
102+
103+
104+
class KDistCleanCommand(Command, LoggingOptions):
105+
target: str
106+
107+
@staticmethod
108+
def name() -> str:
109+
return 'clean'
110+
111+
@staticmethod
112+
def help_str() -> str:
113+
return 'clean targets'
114+
115+
@staticmethod
116+
def default() -> dict[str, Any]:
117+
return {
118+
'target': None,
119+
}
120+
121+
@staticmethod
122+
def update_args(parser: ArgumentParser) -> None:
123+
parser.add_argument(
124+
'target',
125+
metavar='TARGET',
126+
nargs='?',
127+
help='target to clean',
128+
)
86129

130+
def exec(self) -> None:
131+
res = kdist.clean(self.target)
132+
print(res)
87133

88-
def _exec_which(target: str | None) -> None:
89-
res = kdist.which(target)
90-
print(res)
91134

135+
class KDistWhichCommand(Command, LoggingOptions):
136+
target: str
92137

93-
def _exec_list() -> None:
94-
targets_by_plugin: dict[str, list[str]] = {}
95-
for plugin_name, target_name in target_ids():
96-
targets = targets_by_plugin.get(plugin_name, [])
97-
targets.append(target_name)
98-
targets_by_plugin[plugin_name] = targets
138+
@staticmethod
139+
def name() -> str:
140+
return 'which'
99141

100-
for plugin_name in targets_by_plugin:
101-
print(plugin_name)
102-
for target_name in targets_by_plugin[plugin_name]:
103-
print(f'* {target_name}')
142+
@staticmethod
143+
def help_str() -> str:
144+
return 'print target location'
104145

146+
@staticmethod
147+
def default() -> dict[str, Any]:
148+
return {
149+
'target': None,
150+
}
105151

106-
def _parse_arguments() -> Namespace:
107-
def add_target_arg(parser: ArgumentParser, help_text: str) -> None:
152+
@staticmethod
153+
def update_args(parser: ArgumentParser) -> None:
108154
parser.add_argument(
109155
'target',
110156
metavar='TARGET',
111157
nargs='?',
112-
help=help_text,
158+
help='target to print directory for',
113159
)
114160

115-
k_cli_args = KCLIArgs()
116-
117-
parser = ArgumentParser(prog='kdist', parents=[k_cli_args.logging_args])
118-
command_parser = parser.add_subparsers(dest='command', required=True)
119-
120-
build_parser = command_parser.add_parser('build', help='build targets')
121-
build_parser.add_argument('targets', metavar='TARGET', nargs='*', default='*', help='target to build')
122-
build_parser.add_argument(
123-
'-a',
124-
'--arg',
125-
dest='args',
126-
metavar='ARG',
127-
action='append',
128-
default=[],
129-
help='build with argument',
130-
)
131-
build_parser.add_argument('-f', '--force', action='store_true', default=False, help='force build')
132-
build_parser.add_argument('-j', '--jobs', metavar='N', type=int, default=1, help='maximal number of build jobs')
161+
def exec(self) -> None:
162+
res = kdist.which(self.target)
163+
print(res)
164+
133165

134-
clean_parser = command_parser.add_parser('clean', help='clean targets')
135-
add_target_arg(clean_parser, 'target to clean')
166+
class KDistListCommand(Command, LoggingOptions):
167+
@staticmethod
168+
def name() -> str:
169+
return 'list'
136170

137-
which_parser = command_parser.add_parser('which', help='print target location')
138-
add_target_arg(which_parser, 'target to print directory for')
171+
@staticmethod
172+
def help_str() -> str:
173+
return 'print list of available targets'
139174

140-
command_parser.add_parser('list', help='print list of available targets')
175+
def exec(self) -> None:
176+
targets_by_plugin: dict[str, list[str]] = {}
177+
for plugin_name, target_name in target_ids():
178+
targets = targets_by_plugin.get(plugin_name, [])
179+
targets.append(target_name)
180+
targets_by_plugin[plugin_name] = targets
141181

142-
return parser.parse_args()
182+
for plugin_name in targets_by_plugin:
183+
print(plugin_name)
184+
for target_name in targets_by_plugin[plugin_name]:
185+
print(f'* {target_name}')
143186

144187

145188
if __name__ == '__main__':

0 commit comments

Comments
 (0)
Please sign in to comment.