umaudemc 0.14.0__py3-none-any.whl → 0.15.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- umaudemc/__init__.py +1 -1
- umaudemc/__main__.py +21 -0
- umaudemc/command/scheck.py +23 -14
- umaudemc/command/sworker.py +187 -0
- umaudemc/command/test.py +2 -62
- umaudemc/common.py +100 -16
- umaudemc/distributed.py +328 -0
- umaudemc/kleene.py +6 -1
- umaudemc/quatex.py +73 -25
- umaudemc/simulators.py +1 -1
- umaudemc/statistical.py +5 -5
- umaudemc/usermsgs.py +6 -0
- {umaudemc-0.14.0.dist-info → umaudemc-0.15.1.dist-info}/METADATA +4 -4
- {umaudemc-0.14.0.dist-info → umaudemc-0.15.1.dist-info}/RECORD +18 -16
- {umaudemc-0.14.0.dist-info → umaudemc-0.15.1.dist-info}/WHEEL +1 -1
- {umaudemc-0.14.0.dist-info → umaudemc-0.15.1.dist-info}/entry_points.txt +0 -0
- {umaudemc-0.14.0.dist-info → umaudemc-0.15.1.dist-info/licenses}/LICENSE +0 -0
- {umaudemc-0.14.0.dist-info → umaudemc-0.15.1.dist-info}/top_level.txt +0 -0
umaudemc/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '0.
|
|
1
|
+
__version__ = '0.15.1'
|
umaudemc/__main__.py
CHANGED
|
@@ -312,6 +312,10 @@ def build_parser():
|
|
|
312
312
|
type=int,
|
|
313
313
|
default=1
|
|
314
314
|
)
|
|
315
|
+
parser_scheck.add_argument(
|
|
316
|
+
'--distribute',
|
|
317
|
+
help='Distribute the computation over some machines'
|
|
318
|
+
)
|
|
315
319
|
parser_scheck.add_argument(
|
|
316
320
|
'--format', '-f',
|
|
317
321
|
help='Output format for the simulation results',
|
|
@@ -326,6 +330,18 @@ def build_parser():
|
|
|
326
330
|
|
|
327
331
|
parser_scheck.set_defaults(mode='scheck')
|
|
328
332
|
|
|
333
|
+
#
|
|
334
|
+
# Statistical model checking distributed worker
|
|
335
|
+
#
|
|
336
|
+
|
|
337
|
+
parser_sworker = subparsers.add_parser('sworker', help='Worker for distributed statistical model checking')
|
|
338
|
+
|
|
339
|
+
parser_sworker.add_argument('--address', '-a', help='listening address', default='127.0.0.1')
|
|
340
|
+
parser_sworker.add_argument('--port', '-p', help='listening port', type=int, default=1234)
|
|
341
|
+
parser_sworker.add_argument('--keep-file', help='keep received Maude file for debugging', action='store_true')
|
|
342
|
+
|
|
343
|
+
parser_sworker.set_defaults(mode='sworker')
|
|
344
|
+
|
|
329
345
|
#
|
|
330
346
|
# Test and benchmark test suites from the command line
|
|
331
347
|
#
|
|
@@ -501,6 +517,11 @@ def main():
|
|
|
501
517
|
from .command.scheck import scheck
|
|
502
518
|
return scheck(args)
|
|
503
519
|
|
|
520
|
+
# Werker for distributed statistical model-checking
|
|
521
|
+
elif args.mode == 'sworker':
|
|
522
|
+
from .command.sworker import sworker
|
|
523
|
+
return sworker(args)
|
|
524
|
+
|
|
504
525
|
# Batch test subcommand
|
|
505
526
|
elif args.mode == 'test':
|
|
506
527
|
from .command.test import test
|
umaudemc/command/scheck.py
CHANGED
|
@@ -19,10 +19,10 @@ def show_results(program, nsims, qdata):
|
|
|
19
19
|
qdata_it = iter(qdata)
|
|
20
20
|
q = next(qdata_it, None)
|
|
21
21
|
|
|
22
|
-
for k, (line, column, params) in enumerate(program.query_locations):
|
|
22
|
+
for k, (fname, line, column, params) in enumerate(program.query_locations):
|
|
23
23
|
# Print the query name and location only if there are many
|
|
24
24
|
if program.nqueries > 1:
|
|
25
|
-
print(f'Query {k + 1} (
|
|
25
|
+
print(f'Query {k + 1} ({fname}:{line}:{column})')
|
|
26
26
|
|
|
27
27
|
# For parametric queries, we show the result for every value
|
|
28
28
|
var = params[0] if params else None
|
|
@@ -136,7 +136,7 @@ def scheck(args):
|
|
|
136
136
|
return 1
|
|
137
137
|
|
|
138
138
|
with open(args.query) as quatex_file:
|
|
139
|
-
program = parse_quatex(quatex_file, filename=args.query)
|
|
139
|
+
program, seen_files = parse_quatex(quatex_file, filename=args.query, legacy=args.assign == 'pmaude')
|
|
140
140
|
|
|
141
141
|
if not program:
|
|
142
142
|
return 1
|
|
@@ -145,14 +145,7 @@ def scheck(args):
|
|
|
145
145
|
usermsgs.print_warning('No queries in the input file.')
|
|
146
146
|
return 0
|
|
147
147
|
|
|
148
|
-
# Get the simulator for the given assignment method
|
|
149
|
-
simulator = get_simulator(args.assign, data)
|
|
150
|
-
|
|
151
|
-
if not simulator:
|
|
152
|
-
return 1
|
|
153
|
-
|
|
154
148
|
# Check the simulation parameters
|
|
155
|
-
|
|
156
149
|
if not (0 <= args.alpha <= 1):
|
|
157
150
|
usermsgs.print_error(f'Wrong value {args.alpha} for the alpha parameter (must be between 0 and 1).')
|
|
158
151
|
return 1
|
|
@@ -170,10 +163,26 @@ def scheck(args):
|
|
|
170
163
|
if min_sim is None and max_sim is None:
|
|
171
164
|
return 1
|
|
172
165
|
|
|
173
|
-
#
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
166
|
+
# Distributed computations follow a different path
|
|
167
|
+
if args.distribute:
|
|
168
|
+
from ..distributed import distributed_check
|
|
169
|
+
|
|
170
|
+
num_sims, qdata = distributed_check(args, data, min_sim, max_sim, program, seen_files)
|
|
171
|
+
|
|
172
|
+
if num_sims is None:
|
|
173
|
+
return 1
|
|
174
|
+
|
|
175
|
+
else:
|
|
176
|
+
# Get the simulator for the given assignment method
|
|
177
|
+
simulator = get_simulator(args.assign, data)
|
|
178
|
+
|
|
179
|
+
if not simulator:
|
|
180
|
+
return 1
|
|
181
|
+
|
|
182
|
+
# Call the statistical model checker
|
|
183
|
+
num_sims, qdata = check(program, simulator,
|
|
184
|
+
args.seed, args.alpha, args.delta, args.block,
|
|
185
|
+
min_sim, max_sim, args.jobs, args.verbose)
|
|
177
186
|
|
|
178
187
|
# Print the results on the terminal
|
|
179
188
|
(show_json if args.format == 'json' else show_results)(program, num_sims, qdata)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Command for a worker of the statistical model checker
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import multiprocessing as mp
|
|
7
|
+
import os
|
|
8
|
+
import random
|
|
9
|
+
import socket
|
|
10
|
+
import sys
|
|
11
|
+
import tarfile
|
|
12
|
+
import tempfile
|
|
13
|
+
from array import array
|
|
14
|
+
|
|
15
|
+
from ..common import maude, parse_initial_data, usermsgs
|
|
16
|
+
from ..quatex import parse_quatex
|
|
17
|
+
from ..simulators import get_simulator
|
|
18
|
+
from ..statistical import run, QueryData, make_parameter_dicts
|
|
19
|
+
|
|
20
|
+
# Python-version-dependent option for safer TAR extraction
|
|
21
|
+
EXTRACT_OPTIONS = {'filter': 'data'} if sys.version_info >= (3, 12) else {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read_json(sock):
|
|
25
|
+
"""Read a JSON value from a socket"""
|
|
26
|
+
|
|
27
|
+
# Read a 32 bit integer for the value length in bytes
|
|
28
|
+
if not (data := sock.recv(4)):
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
length = int.from_bytes(data, 'big')
|
|
32
|
+
|
|
33
|
+
# Read the JSON to a byte string
|
|
34
|
+
data = b''
|
|
35
|
+
|
|
36
|
+
while len(data) < length:
|
|
37
|
+
data += sock.recv(length)
|
|
38
|
+
|
|
39
|
+
return json.loads(data.decode())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Dummy:
|
|
43
|
+
"""Dummy class to be used as a namespace"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, values):
|
|
46
|
+
self.__dict__ = values
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Worker:
|
|
50
|
+
"""Worker for simulations"""
|
|
51
|
+
|
|
52
|
+
def __init__(self):
|
|
53
|
+
self.program = None
|
|
54
|
+
self.simulator = None
|
|
55
|
+
self.block = 100
|
|
56
|
+
|
|
57
|
+
def setup(self, tmp_dir, args):
|
|
58
|
+
"""Setup the execution environment"""
|
|
59
|
+
|
|
60
|
+
# Copy parameters
|
|
61
|
+
self.block = args.block
|
|
62
|
+
|
|
63
|
+
maude.setRandomSeed(args.seed)
|
|
64
|
+
random.seed(args.seed)
|
|
65
|
+
|
|
66
|
+
# Do the same as the scheck command without checks
|
|
67
|
+
args.file = os.path.join(tmp_dir, args.file)
|
|
68
|
+
|
|
69
|
+
if not (data := parse_initial_data(args)):
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
with open(os.path.join(tmp_dir, args.query)) as quatex_file:
|
|
73
|
+
self.program, _ = parse_quatex(quatex_file, filename=args.query,
|
|
74
|
+
legacy=args.assign == 'pmaude')
|
|
75
|
+
|
|
76
|
+
if not self.program:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
# Get the simulator for the given assignment method
|
|
80
|
+
self.simulator = get_simulator(args.assign, data)
|
|
81
|
+
|
|
82
|
+
if not self.simulator:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
def run(self, conn):
|
|
88
|
+
"""Run the simulation until it is finished"""
|
|
89
|
+
|
|
90
|
+
program = self.program
|
|
91
|
+
simulator = self.simulator
|
|
92
|
+
block = self.block
|
|
93
|
+
|
|
94
|
+
# Query data
|
|
95
|
+
qdata = [QueryData(k, idict)
|
|
96
|
+
for k, qinfo in enumerate(program.query_locations)
|
|
97
|
+
for idict in make_parameter_dicts(qinfo[3])]
|
|
98
|
+
|
|
99
|
+
sums = array('d', [0.0] * len(qdata))
|
|
100
|
+
sum_sq = array('d', [0.0] * len(qdata))
|
|
101
|
+
|
|
102
|
+
while True:
|
|
103
|
+
|
|
104
|
+
for _ in range(block):
|
|
105
|
+
# Run the simulation and compute all queries at once
|
|
106
|
+
values = run(program, qdata, simulator)
|
|
107
|
+
|
|
108
|
+
for k in range(len(qdata)):
|
|
109
|
+
sums[k] += values[k]
|
|
110
|
+
sum_sq[k] += values[k] * values[k]
|
|
111
|
+
|
|
112
|
+
conn.send(b'b' + sums.tobytes() + sum_sq.tobytes())
|
|
113
|
+
|
|
114
|
+
# Check whether to continue
|
|
115
|
+
answer = conn.recv(1)
|
|
116
|
+
|
|
117
|
+
if answer == b's':
|
|
118
|
+
print('Done')
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
elif answer != b'c':
|
|
122
|
+
usermsgs.print_error(f'Unknown command {answer.decode()}. Stopping.')
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
for k in range(len(qdata)):
|
|
126
|
+
sums[k] = 0
|
|
127
|
+
sum_sq[k] = 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def handle_request(message, conn, addr, keep_file):
|
|
131
|
+
"""Handle a request in a separate process"""
|
|
132
|
+
|
|
133
|
+
command = Dummy(message)
|
|
134
|
+
|
|
135
|
+
with tempfile.TemporaryDirectory(delete=not keep_file) as tmp_dir:
|
|
136
|
+
# Print the temporary working directory for debugging purposes
|
|
137
|
+
if keep_file:
|
|
138
|
+
print('Temporary directory:', tmp_dir)
|
|
139
|
+
|
|
140
|
+
# Recover the required files
|
|
141
|
+
with conn.makefile('rb', buffering=0) as fobj:
|
|
142
|
+
with tarfile.open(mode='r|*', fileobj=fobj) as tarf:
|
|
143
|
+
tarf.extractall(tmp_dir, **EXTRACT_OPTIONS)
|
|
144
|
+
|
|
145
|
+
# Setup a worker object
|
|
146
|
+
worker = Worker()
|
|
147
|
+
|
|
148
|
+
if not worker.setup(tmp_dir, command):
|
|
149
|
+
usermsgs.print_error(f'{addr}: bad request from scheck')
|
|
150
|
+
conn.send(b'e')
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
# Send confirmation
|
|
154
|
+
conn.send(b'o')
|
|
155
|
+
|
|
156
|
+
# Wait for start signal
|
|
157
|
+
conn.recv(1)
|
|
158
|
+
|
|
159
|
+
worker.run(conn)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def sworker(args):
|
|
163
|
+
"""Worker for distributed statistical model checking"""
|
|
164
|
+
|
|
165
|
+
# Parse the listing address
|
|
166
|
+
try:
|
|
167
|
+
with socket.create_server((args.address, args.port), backlog=1) as sock:
|
|
168
|
+
while True:
|
|
169
|
+
print(f'👂 Listening on {args.address}:{args.port}...')
|
|
170
|
+
conn, addr = sock.accept()
|
|
171
|
+
|
|
172
|
+
usermsgs.print_info(f'Accepted connection from {":".join(map(str, addr))}.')
|
|
173
|
+
|
|
174
|
+
while True:
|
|
175
|
+
# Read the initiation message
|
|
176
|
+
if (message := read_json(conn)) is None:
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
# A separate process to cleanup Maude state
|
|
180
|
+
process = mp.Process(target=handle_request, args=(message, conn, addr, args.keep_file))
|
|
181
|
+
process.start()
|
|
182
|
+
process.join()
|
|
183
|
+
|
|
184
|
+
except KeyboardInterrupt:
|
|
185
|
+
print('Server closed by the user.')
|
|
186
|
+
|
|
187
|
+
return 0
|
umaudemc/command/test.py
CHANGED
|
@@ -14,7 +14,7 @@ import sys # To find the Python executable path
|
|
|
14
14
|
import threading # To support timeouts
|
|
15
15
|
import time # To measure time
|
|
16
16
|
|
|
17
|
-
from ..common import maude, usermsgs
|
|
17
|
+
from ..common import maude, usermsgs, load_specification
|
|
18
18
|
from ..formulae import Parser, collect_aprops
|
|
19
19
|
from ..backends import supported_logics, get_backends, backend_for
|
|
20
20
|
from ..terminal import terminal as tmn
|
|
@@ -62,67 +62,7 @@ class TestCase:
|
|
|
62
62
|
def load_cases(filename):
|
|
63
63
|
"""Load test cases from YAML or JSON specifications"""
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# The YAML package is only loaded when needed
|
|
68
|
-
# (pyaml is an optional dependency)
|
|
69
|
-
if extension in ('.yaml', '.yml'):
|
|
70
|
-
try:
|
|
71
|
-
import yaml
|
|
72
|
-
from yaml.loader import SafeLoader
|
|
73
|
-
|
|
74
|
-
except ImportError:
|
|
75
|
-
usermsgs.print_error(
|
|
76
|
-
'Cannot load cases from YAML file, since the yaml package is not installed.\n'
|
|
77
|
-
'Please convert the YAML to JSON or install it with pip install pyaml.')
|
|
78
|
-
return None
|
|
79
|
-
|
|
80
|
-
# The YAML loader is replaced so that entities have its line number
|
|
81
|
-
# associated to print more useful messages. This is not possible with
|
|
82
|
-
# the standard JSON library.
|
|
83
|
-
|
|
84
|
-
class SafeLineLoader(SafeLoader):
|
|
85
|
-
def construct_mapping(self, node, deep=False):
|
|
86
|
-
mapping = super(SafeLineLoader, self).construct_mapping(node, deep=deep)
|
|
87
|
-
# Add 1 so line numbering starts at 1
|
|
88
|
-
mapping['__line__'] = node.start_mark.line + 1
|
|
89
|
-
return mapping
|
|
90
|
-
|
|
91
|
-
try:
|
|
92
|
-
with open(filename) as caspec:
|
|
93
|
-
return yaml.load(caspec, Loader=SafeLineLoader)
|
|
94
|
-
|
|
95
|
-
except yaml.error.YAMLError as ype:
|
|
96
|
-
usermsgs.print_error(f'Error while parsing test file: {ype}.')
|
|
97
|
-
|
|
98
|
-
# TOML format
|
|
99
|
-
if extension == '.toml':
|
|
100
|
-
try:
|
|
101
|
-
import tomllib
|
|
102
|
-
|
|
103
|
-
except ImportError:
|
|
104
|
-
usermsgs.print_error(
|
|
105
|
-
'Cannot load cases from TOML file, '
|
|
106
|
-
'which is only available since Python 3.10.')
|
|
107
|
-
return None
|
|
108
|
-
|
|
109
|
-
try:
|
|
110
|
-
with open(filename) as caspec:
|
|
111
|
-
return tomllib.load(caspec)
|
|
112
|
-
|
|
113
|
-
except tomllib.TOMLDecodeError as tde:
|
|
114
|
-
usermsgs.print_error(f'Error while parsing test file: {tde}.')
|
|
115
|
-
|
|
116
|
-
# JSON format
|
|
117
|
-
else:
|
|
118
|
-
try:
|
|
119
|
-
with open(filename) as caspec:
|
|
120
|
-
return json.load(caspec)
|
|
121
|
-
|
|
122
|
-
except json.JSONDecodeError as jde:
|
|
123
|
-
usermsgs.print_error(f'Error while parsing test file: {jde}.')
|
|
124
|
-
|
|
125
|
-
return None
|
|
65
|
+
return load_specification(filename, 'cases')
|
|
126
66
|
|
|
127
67
|
|
|
128
68
|
def read_suite(filename, from_file=None, skip_case=0):
|
umaudemc/common.py
CHANGED
|
@@ -44,27 +44,43 @@ def find_maude_file_abs(filename):
|
|
|
44
44
|
return None
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
"""
|
|
49
|
-
if os.path.isabs(filename):
|
|
50
|
-
return find_maude_file_abs(filename)
|
|
47
|
+
class MaudeFileFinder:
|
|
48
|
+
"""Locate Maude files as Maude does"""
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
paths = [os.getcwd(), os.path.dirname(maude.__file__)]
|
|
50
|
+
MAUDE_STD = {'file', 'linear', 'machine-int', 'metaInterpreter', 'model-checker',
|
|
51
|
+
'prelude', 'prng', 'process', 'smt', 'socket', 'term-order', 'time'}
|
|
55
52
|
|
|
56
|
-
maudelib = os.getenv('MAUDE_LIB')
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
def __init__(self):
|
|
55
|
+
# Maude also considers the current working directory
|
|
56
|
+
# and the directory of the Maude binary
|
|
57
|
+
self.paths = [os.path.dirname(maude.__file__),
|
|
58
|
+
*os.getenv('MAUDE_LIB', '').split(os.pathsep)]
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
fullname = find_maude_file_abs(abspath)
|
|
64
|
-
if fullname is not None:
|
|
65
|
-
return fullname
|
|
60
|
+
def _is_std(self, name):
|
|
61
|
+
"""Is this a file from the Maude distribution"""
|
|
66
62
|
|
|
67
|
-
|
|
63
|
+
return name in self.MAUDE_STD or f'{name}.maude' in self.MAUDE_STD
|
|
64
|
+
|
|
65
|
+
def find(self, name, cwd):
|
|
66
|
+
# Absolute path, no ambiguity
|
|
67
|
+
if os.path.isabs(name):
|
|
68
|
+
return find_maude_file_abs(name), False
|
|
69
|
+
|
|
70
|
+
for path in (cwd, *self.paths):
|
|
71
|
+
abspath = os.path.join(path, name)
|
|
72
|
+
fullname = find_maude_file_abs(abspath)
|
|
73
|
+
|
|
74
|
+
if fullname is not None:
|
|
75
|
+
return fullname, path != cwd and self._is_std(name)
|
|
76
|
+
|
|
77
|
+
return None, None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def find_maude_file(filename):
|
|
81
|
+
"""Find a Maude file taking MAUDE_LIB into account"""
|
|
82
|
+
|
|
83
|
+
return MaudeFileFinder().find(filename, os.getcwd())[0]
|
|
68
84
|
|
|
69
85
|
|
|
70
86
|
def parse_initial_data(args):
|
|
@@ -191,3 +207,71 @@ def default_model_settings(logic, purge_fails, merge_states, strategy, tableau=F
|
|
|
191
207
|
merge_states = 'state' if logic.startswith('CTL') else ('edge' if logic == 'Mucalc' else 'no')
|
|
192
208
|
|
|
193
209
|
return purge_fails, merge_states
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def load_specification(filename, topic):
|
|
213
|
+
"""Load specifications from YAML or JSON files"""
|
|
214
|
+
|
|
215
|
+
extension = os.path.splitext(filename)[1]
|
|
216
|
+
|
|
217
|
+
# The YAML package is only loaded when needed
|
|
218
|
+
# (pyaml is an optional dependency)
|
|
219
|
+
if extension in ('.yaml', '.yml'):
|
|
220
|
+
try:
|
|
221
|
+
import yaml
|
|
222
|
+
from yaml.loader import SafeLoader
|
|
223
|
+
|
|
224
|
+
except ImportError:
|
|
225
|
+
usermsgs.print_error(
|
|
226
|
+
f'Cannot load {topic} from YAML file, since the yaml package is not installed.\n'
|
|
227
|
+
'Please convert the YAML to JSON or install it with pip install pyaml.')
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
# The YAML loader is replaced so that entities have its line number
|
|
231
|
+
# associated to print more useful messages. This is not possible with
|
|
232
|
+
# the standard JSON library.
|
|
233
|
+
|
|
234
|
+
class SafeLineLoader(SafeLoader):
|
|
235
|
+
def construct_mapping(self, node, deep=False):
|
|
236
|
+
mapping = super(SafeLineLoader, self).construct_mapping(node, deep=deep)
|
|
237
|
+
# Add 1 so line numbering starts at 1
|
|
238
|
+
mapping['__line__'] = node.start_mark.line + 1
|
|
239
|
+
return mapping
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
with open(filename) as caspec:
|
|
243
|
+
return yaml.load(caspec, Loader=SafeLineLoader)
|
|
244
|
+
|
|
245
|
+
except yaml.error.YAMLError as ype:
|
|
246
|
+
usermsgs.print_error(f'Error while parsing {topic} file: {ype}.')
|
|
247
|
+
|
|
248
|
+
# TOML format
|
|
249
|
+
if extension == '.toml':
|
|
250
|
+
try:
|
|
251
|
+
import tomllib
|
|
252
|
+
|
|
253
|
+
except ImportError:
|
|
254
|
+
usermsgs.print_error(
|
|
255
|
+
f'Cannot load {topic} from TOML file, '
|
|
256
|
+
'which is only available since Python 3.10.')
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
with open(filename, 'rb') as caspec:
|
|
261
|
+
return tomllib.load(caspec)
|
|
262
|
+
|
|
263
|
+
except tomllib.TOMLDecodeError as tde:
|
|
264
|
+
usermsgs.print_error(f'Error while parsing {topic} file: {tde}.')
|
|
265
|
+
|
|
266
|
+
# JSON format
|
|
267
|
+
else:
|
|
268
|
+
import json
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
with open(filename) as caspec:
|
|
272
|
+
return json.load(caspec)
|
|
273
|
+
|
|
274
|
+
except json.JSONDecodeError as jde:
|
|
275
|
+
usermsgs.print_error(f'Error while parsing {topic} file: {jde}.')
|
|
276
|
+
|
|
277
|
+
return None
|
umaudemc/distributed.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Distributed model checking
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import random
|
|
9
|
+
import re
|
|
10
|
+
import selectors
|
|
11
|
+
import socket
|
|
12
|
+
import tarfile
|
|
13
|
+
from array import array
|
|
14
|
+
from contextlib import ExitStack
|
|
15
|
+
|
|
16
|
+
from .common import load_specification, usermsgs, MaudeFileFinder
|
|
17
|
+
from .statistical import QueryData, check_interval, get_quantile_func, make_parameter_dicts
|
|
18
|
+
|
|
19
|
+
# Regular expression for Maude inclusions
|
|
20
|
+
LOAD_REGEX = re.compile(rb'^\s*s?load\s+("((?:[^"\\]|\\.)+)"|\S+)')
|
|
21
|
+
EOF_REGEX = re.compile(rb'^\s*eof($|\s)')
|
|
22
|
+
TOP_COMMENT_REGEX = re.compile(rb'(?:\*{3}|-{3})\s*(.)')
|
|
23
|
+
NESTED_COMMENT_REGEX = re.compile(rb'[\(\)]')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def strip_comments(fobj):
|
|
27
|
+
"""Strip Maude comments from a file"""
|
|
28
|
+
|
|
29
|
+
comment_depth = 0 # depth of nested multiline comments
|
|
30
|
+
chunks = []
|
|
31
|
+
|
|
32
|
+
for line in fobj:
|
|
33
|
+
start = 0
|
|
34
|
+
|
|
35
|
+
while True:
|
|
36
|
+
# Inside a multiline comment
|
|
37
|
+
if comment_depth > 0:
|
|
38
|
+
# Find a the start or end of a multiline comment
|
|
39
|
+
if m := NESTED_COMMENT_REGEX.search(line, start):
|
|
40
|
+
if m.group(0) == b')': # end
|
|
41
|
+
comment_depth -= 1
|
|
42
|
+
|
|
43
|
+
else: # start
|
|
44
|
+
comment_depth += 1
|
|
45
|
+
|
|
46
|
+
start = m.end()
|
|
47
|
+
|
|
48
|
+
# The whole line is inside a multiline comment
|
|
49
|
+
else:
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
# Not inside a comment
|
|
53
|
+
else:
|
|
54
|
+
if m := TOP_COMMENT_REGEX.search(line, start):
|
|
55
|
+
chunks.append(line[start:m.start()] + b'\n')
|
|
56
|
+
|
|
57
|
+
# Start of multiline comment
|
|
58
|
+
if m.group(1) == b'(':
|
|
59
|
+
start = m.end()
|
|
60
|
+
comment_depth += 1
|
|
61
|
+
|
|
62
|
+
# Single line comment
|
|
63
|
+
else:
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
# No comment at all
|
|
67
|
+
else:
|
|
68
|
+
chunks.append(line[start:])
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
if chunks:
|
|
72
|
+
yield b' '.join(chunks)
|
|
73
|
+
chunks.clear()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def flatten_maude_file(filename, fobj):
|
|
77
|
+
"""Scan sources for its dependencies"""
|
|
78
|
+
|
|
79
|
+
# Maude file finder
|
|
80
|
+
mfinder = MaudeFileFinder()
|
|
81
|
+
|
|
82
|
+
# Files are explored depth-first
|
|
83
|
+
stack = [(lambda v: (filename, v, strip_comments(v)))(open(filename, 'rb'))]
|
|
84
|
+
# Files already included to avoid double inclusion (loads are interpreted as sloads)
|
|
85
|
+
seen = {os.path.realpath(filename)}
|
|
86
|
+
|
|
87
|
+
while stack:
|
|
88
|
+
fname, file, lines = stack[-1]
|
|
89
|
+
|
|
90
|
+
while line := next(lines, None):
|
|
91
|
+
# Load commands are only allowed at the beginning of a line
|
|
92
|
+
if m := LOAD_REGEX.match(line):
|
|
93
|
+
# File name (with quotes, maybe)
|
|
94
|
+
next_fname = m.group(1).decode()
|
|
95
|
+
|
|
96
|
+
if next_fname[0] == '"':
|
|
97
|
+
next_fname = next_fname[1:-1].replace(r'\"', '"')
|
|
98
|
+
|
|
99
|
+
# Inclusions are tricky
|
|
100
|
+
next_path, is_std = mfinder.find(next_fname, os.path.dirname(fname))
|
|
101
|
+
|
|
102
|
+
# For standard files, we preserve the inclusion
|
|
103
|
+
if is_std:
|
|
104
|
+
fobj.write(line)
|
|
105
|
+
|
|
106
|
+
# Otherwise, we copy the file unless already done
|
|
107
|
+
elif next_path not in seen:
|
|
108
|
+
next_file = open(next_path, 'rb')
|
|
109
|
+
stack.append((next_path, next_file, strip_comments(next_file)))
|
|
110
|
+
seen.add(next_path)
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
elif EOF_REGEX.match(line):
|
|
114
|
+
line = None
|
|
115
|
+
break
|
|
116
|
+
else:
|
|
117
|
+
fobj.write(line)
|
|
118
|
+
|
|
119
|
+
# Whether the file is exhausted
|
|
120
|
+
if line is None:
|
|
121
|
+
fobj.write(b'\n') # just in case there is no line break at end of file
|
|
122
|
+
stack.pop()
|
|
123
|
+
file.close()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def process_dspec(dspec, fname):
|
|
127
|
+
"""Normalize a distributed SMC specification"""
|
|
128
|
+
|
|
129
|
+
if not isinstance(dspec, dict):
|
|
130
|
+
usermsgs.print_error_file('the distribution specification must be a dictionary.', fname)
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
# Normalize workers to a dictionary
|
|
134
|
+
workers = dspec.get('workers')
|
|
135
|
+
|
|
136
|
+
if not isinstance(workers, list):
|
|
137
|
+
usermsgs.print_error_file('the distribution specification does not contain a list-valued \'workers\' key.', fname)
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
for k, worker in enumerate(workers):
|
|
141
|
+
# Strings address:port are allowed
|
|
142
|
+
if isinstance(worker, str):
|
|
143
|
+
try:
|
|
144
|
+
address, port = worker.split(':')
|
|
145
|
+
worker = {'address': address, 'port': int(port)}
|
|
146
|
+
|
|
147
|
+
except ValueError:
|
|
148
|
+
usermsgs.print_error_file(f'bad address specification {worker} for worker {k + 1} '
|
|
149
|
+
'(it should be <address>:<port>).', fname)
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
workers[k] = worker
|
|
153
|
+
|
|
154
|
+
# Otherwise, it must be a dictionary
|
|
155
|
+
elif not isinstance(worker, dict):
|
|
156
|
+
usermsgs.print_error_file(f'the specification for worker {k + 1} is not a dictionary.', fname)
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
# With address and port keys
|
|
160
|
+
else:
|
|
161
|
+
for key, ktype in (('address', str), ('port', int)):
|
|
162
|
+
if key not in worker:
|
|
163
|
+
usermsgs.print_error_file(f'missing key \'{key}\' for worker {k + 1}.', fname)
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
if not isinstance(worker[key], ktype):
|
|
167
|
+
usermsgs.print_error_file(f'wrong type for key \'{key}\' in worker {k + 1}, {ktype.__name__} expected.', fname)
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
# Name just for reference in errors and messages
|
|
171
|
+
if 'name' not in worker:
|
|
172
|
+
worker['name'] = f'{worker["address"]}:{worker["port"]}'
|
|
173
|
+
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def setup_workers(args, initial_data, dspec, seen_files, stack):
|
|
178
|
+
"""Setup workers and send problem data"""
|
|
179
|
+
|
|
180
|
+
workers = dspec['workers']
|
|
181
|
+
|
|
182
|
+
# Generate a random seed for each worker
|
|
183
|
+
random.seed(args.seed)
|
|
184
|
+
|
|
185
|
+
seeds = [random.getrandbits(20) for _ in range(len(workers))]
|
|
186
|
+
|
|
187
|
+
# Data to be passed to the external machine
|
|
188
|
+
COPY = ('initial', 'strategy', 'module', 'metamodule', 'opaque', 'full_matchrew',
|
|
189
|
+
'purge_fails', 'merge_states', 'assign', 'block', 'query', 'assign', 'advise', 'verbose')
|
|
190
|
+
|
|
191
|
+
data = {key: args.__dict__[key] for key in COPY} | {'file': 'source.maude'}
|
|
192
|
+
|
|
193
|
+
# Make a flattened version of the Maude file
|
|
194
|
+
flat_source = io.BytesIO()
|
|
195
|
+
flatten_maude_file(initial_data.filename, flat_source)
|
|
196
|
+
|
|
197
|
+
flat_info = tarfile.TarInfo('source.maude')
|
|
198
|
+
flat_info.size = flat_source.getbuffer().nbytes
|
|
199
|
+
|
|
200
|
+
# Save the sockets for each worker
|
|
201
|
+
sockets = []
|
|
202
|
+
|
|
203
|
+
for worker, seed in zip(workers, seeds):
|
|
204
|
+
address, port = worker['address'], worker['port']
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
sock = socket.create_connection((address, int(port)))
|
|
208
|
+
|
|
209
|
+
except ConnectionRefusedError:
|
|
210
|
+
usermsgs.print_error(f'Connection refused by worker \'{worker["name"]}\'.')
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
stack.enter_context(sock)
|
|
214
|
+
sockets.append(sock)
|
|
215
|
+
|
|
216
|
+
# Send the input data
|
|
217
|
+
input_data = data | {'seed': seed}
|
|
218
|
+
|
|
219
|
+
if block_size := worker.get('block'):
|
|
220
|
+
input_data['block'] = block_size # if specified
|
|
221
|
+
|
|
222
|
+
input_data = json.dumps(input_data).encode()
|
|
223
|
+
sock.sendall(len(input_data).to_bytes(4) + input_data)
|
|
224
|
+
|
|
225
|
+
# Send the relevant files
|
|
226
|
+
with sock.makefile('wb', buffering=0) as fobj:
|
|
227
|
+
with tarfile.open(mode='w|gz', fileobj=fobj) as tarf:
|
|
228
|
+
flat_source.seek(0)
|
|
229
|
+
tarf.addfile(flat_info, flat_source)
|
|
230
|
+
|
|
231
|
+
for file in seen_files:
|
|
232
|
+
relpath = os.path.relpath(file)
|
|
233
|
+
|
|
234
|
+
if relpath.startswith('..'):
|
|
235
|
+
usermsgs.print_error('QuaTEx file outside the working tree, it will not be included and the execution will fail.')
|
|
236
|
+
else:
|
|
237
|
+
tarf.add(relpath)
|
|
238
|
+
|
|
239
|
+
fobj.flush()
|
|
240
|
+
|
|
241
|
+
# Receive confirmation from the remote
|
|
242
|
+
answer = sock.recv(1)
|
|
243
|
+
|
|
244
|
+
if answer != b'o':
|
|
245
|
+
usermsgs.print_error(f'Configuration error in {worker["name"]} worker.')
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
return sockets
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def distributed_check(args, initial_data, min_sim, max_sim, program, seen_files):
|
|
252
|
+
"""Distributed statistical model checking"""
|
|
253
|
+
|
|
254
|
+
# Load the distribution specification
|
|
255
|
+
if (dspec := load_specification(args.distribute, 'distribution specification')) is None \
|
|
256
|
+
or not process_dspec(dspec, args.distribute):
|
|
257
|
+
return None, None
|
|
258
|
+
|
|
259
|
+
# Gather all sockets in a context to close them when we finish
|
|
260
|
+
with ExitStack() as stack:
|
|
261
|
+
|
|
262
|
+
# Socket to connect with the workers
|
|
263
|
+
if not (sockets := setup_workers(args, initial_data, dspec, seen_files, stack)):
|
|
264
|
+
return None, None
|
|
265
|
+
|
|
266
|
+
print('All workers are ready. Starting...')
|
|
267
|
+
|
|
268
|
+
# Use a selector to wait for updates from any worker
|
|
269
|
+
selector = selectors.DefaultSelector()
|
|
270
|
+
|
|
271
|
+
for sock, data in zip(sockets, dspec['workers']):
|
|
272
|
+
selector.register(sock, selectors.EVENT_READ, data={'block': args.block} | data)
|
|
273
|
+
sock.send(b'c')
|
|
274
|
+
|
|
275
|
+
buffer = array('d')
|
|
276
|
+
|
|
277
|
+
# Query data
|
|
278
|
+
qdata = [QueryData(k, idict)
|
|
279
|
+
for k, qinfo in enumerate(program.query_locations)
|
|
280
|
+
for idict in make_parameter_dicts(qinfo[3])]
|
|
281
|
+
nqueries = len(qdata)
|
|
282
|
+
num_sims = 0
|
|
283
|
+
|
|
284
|
+
quantile = get_quantile_func()
|
|
285
|
+
|
|
286
|
+
while sockets:
|
|
287
|
+
events = selector.select()
|
|
288
|
+
finished = []
|
|
289
|
+
|
|
290
|
+
for key, _ in events:
|
|
291
|
+
sock = key.fileobj
|
|
292
|
+
|
|
293
|
+
answer = sock.recv(1)
|
|
294
|
+
|
|
295
|
+
if answer == b'b':
|
|
296
|
+
data = sock.recv(16 * nqueries)
|
|
297
|
+
buffer.frombytes(data)
|
|
298
|
+
|
|
299
|
+
for k in range(nqueries):
|
|
300
|
+
qdata[k].sum += buffer[k]
|
|
301
|
+
qdata[k].sum_sq += buffer[nqueries + k]
|
|
302
|
+
|
|
303
|
+
num_sims += key.data['block']
|
|
304
|
+
|
|
305
|
+
del buffer[:]
|
|
306
|
+
finished.append(key.fileobj)
|
|
307
|
+
|
|
308
|
+
else:
|
|
309
|
+
usermsgs.print_error(f'Server {key.data["name"]} disconnected or misbehaving')
|
|
310
|
+
selector.unregister(key.fileobj)
|
|
311
|
+
sockets.remove(key.fileobj)
|
|
312
|
+
|
|
313
|
+
# Check whether the simulation has converged
|
|
314
|
+
converged = check_interval(qdata, num_sims, args.alpha, args.delta, quantile, args.verbose)
|
|
315
|
+
|
|
316
|
+
if converged or max_sim and num_sims >= max_sim:
|
|
317
|
+
break
|
|
318
|
+
|
|
319
|
+
for sock in finished:
|
|
320
|
+
sock.send(b'c')
|
|
321
|
+
|
|
322
|
+
finished.clear()
|
|
323
|
+
|
|
324
|
+
# Send stop signal to all workers
|
|
325
|
+
for sock in sockets:
|
|
326
|
+
sock.send(b's')
|
|
327
|
+
|
|
328
|
+
return num_sims, qdata
|
umaudemc/kleene.py
CHANGED
|
@@ -13,6 +13,10 @@ class KleeneExecutionState(GraphExecutionState):
|
|
|
13
13
|
super().__init__(term, pc, stack, conditional, graph_node,
|
|
14
14
|
(extra, set() if iterations is None else iterations))
|
|
15
15
|
|
|
16
|
+
# The second position of the extra attribute is a set of iteration tags of the
|
|
17
|
+
# form ((pc, ctx), enters) where (pc, ctx) identifies the iteration and enters
|
|
18
|
+
# indicates whether it enters or leaves
|
|
19
|
+
|
|
16
20
|
def copy(self, term=None, pc=None, stack=None, conditional=False, graph_node=None, extra=None):
|
|
17
21
|
"""Clone state with possibly some changes"""
|
|
18
22
|
|
|
@@ -44,6 +48,7 @@ class KleeneRunner(GraphRunner):
|
|
|
44
48
|
|
|
45
49
|
def kleene(self, args, stack):
|
|
46
50
|
"""Keep track of iterations"""
|
|
51
|
+
# Identify each dynamic context with a number
|
|
47
52
|
context_id = self.iter_contexts.setdefault(self.current_state.stack, len(self.iter_contexts))
|
|
48
53
|
self.current_state.add_kleene(((args[0], context_id), args[1]))
|
|
49
54
|
super().kleene(args, stack)
|
|
@@ -119,7 +124,7 @@ class StrategyKleeneGraph:
|
|
|
119
124
|
def expand(self):
|
|
120
125
|
"""Expand the underlying graph"""
|
|
121
126
|
|
|
122
|
-
for
|
|
127
|
+
for state in self.state_list:
|
|
123
128
|
for child in state.children:
|
|
124
129
|
if child not in self.state_map:
|
|
125
130
|
self.state_map[child] = len(self.state_list)
|
umaudemc/quatex.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#
|
|
4
4
|
|
|
5
5
|
import ast
|
|
6
|
+
import os
|
|
6
7
|
|
|
7
8
|
from . import usermsgs
|
|
8
9
|
|
|
@@ -21,7 +22,7 @@ class QuaTExProgram:
|
|
|
21
22
|
# Number of queries
|
|
22
23
|
self.nqueries = len(slots) - ndefs
|
|
23
24
|
|
|
24
|
-
# Query information (line, column, and parameters)
|
|
25
|
+
# Query information (file name, line, column, and parameters)
|
|
25
26
|
self.query_locations = qinfo
|
|
26
27
|
|
|
27
28
|
|
|
@@ -33,9 +34,10 @@ class QuaTExLexer:
|
|
|
33
34
|
LT_STRING = 2
|
|
34
35
|
LT_OTHER = 3
|
|
35
36
|
|
|
36
|
-
def __init__(self, source, buffer_size=512):
|
|
37
|
+
def __init__(self, source, filename, buffer_size=512):
|
|
37
38
|
# Input stream (file-like object)
|
|
38
39
|
self.source = source
|
|
40
|
+
self.filename = filename
|
|
39
41
|
# We read in chunks of buffer_size
|
|
40
42
|
self.buffer_size = buffer_size
|
|
41
43
|
self.buffer = self.source.read(self.buffer_size)
|
|
@@ -221,10 +223,15 @@ class QuaTExParser:
|
|
|
221
223
|
UNARY_OPS = ('!', )
|
|
222
224
|
UNARY_AST = (ast.Not, )
|
|
223
225
|
|
|
224
|
-
def __init__(self, source, filename='<stdin>'):
|
|
225
|
-
self.lexer = QuaTExLexer(source)
|
|
226
|
+
def __init__(self, source, filename='<stdin>', legacy=False):
|
|
226
227
|
# Filename is only used for diagnostics
|
|
227
|
-
self.
|
|
228
|
+
self.lexer = QuaTExLexer(source, filename)
|
|
229
|
+
# PMaude legacy syntax
|
|
230
|
+
self.legacy = legacy
|
|
231
|
+
|
|
232
|
+
# File inclusion support
|
|
233
|
+
self.pending_lexers = []
|
|
234
|
+
self.seen_files = set() if filename.startswith('<') else {os.path.realpath(filename)}
|
|
228
235
|
|
|
229
236
|
# Parameters of the current function
|
|
230
237
|
self.fvars = []
|
|
@@ -246,10 +253,10 @@ class QuaTExParser:
|
|
|
246
253
|
self.queries = []
|
|
247
254
|
self.defs = []
|
|
248
255
|
|
|
249
|
-
def _eprint(self, msg, line=None, column=None):
|
|
256
|
+
def _eprint(self, msg, line=None, column=None, fname=None):
|
|
250
257
|
"""Print an error message with line information"""
|
|
251
258
|
|
|
252
|
-
usermsgs.print_error_loc(self.filename,
|
|
259
|
+
usermsgs.print_error_loc(fname or self.lexer.filename,
|
|
253
260
|
line or self.lexer.sline,
|
|
254
261
|
column or self.lexer.scolumn,
|
|
255
262
|
msg)
|
|
@@ -275,6 +282,19 @@ class QuaTExParser:
|
|
|
275
282
|
|
|
276
283
|
return self.stack and self.stack[-1] == state
|
|
277
284
|
|
|
285
|
+
def _next_token(self):
|
|
286
|
+
"""Get the next token without potential file exhaustion"""
|
|
287
|
+
|
|
288
|
+
token = self.lexer.get_token()
|
|
289
|
+
|
|
290
|
+
while self.lexer.exhausted and self.pending_lexers:
|
|
291
|
+
self.lexer, open_file = self.pending_lexers.pop()
|
|
292
|
+
open_file.close()
|
|
293
|
+
|
|
294
|
+
token = self.lexer.get_token()
|
|
295
|
+
|
|
296
|
+
return token
|
|
297
|
+
|
|
278
298
|
def _do_binop(self, op_index, left, right):
|
|
279
299
|
"""Build a binary operator in the AST"""
|
|
280
300
|
|
|
@@ -482,7 +502,7 @@ class QuaTExParser:
|
|
|
482
502
|
line, column = self.lexer.sline, self.lexer.scolumn
|
|
483
503
|
next_token = self.lexer.get_token()
|
|
484
504
|
|
|
485
|
-
# A call to s.
|
|
505
|
+
# A call to s.rval
|
|
486
506
|
if token == 's' and next_token == '.':
|
|
487
507
|
if not self._expect('rval', '('):
|
|
488
508
|
return None
|
|
@@ -490,7 +510,8 @@ class QuaTExParser:
|
|
|
490
510
|
token = self.lexer.get_token()
|
|
491
511
|
ltype = self.lexer.ltype
|
|
492
512
|
|
|
493
|
-
if ltype not in (self.lexer.LT_NAME, self.lexer.LT_STRING
|
|
513
|
+
if ltype not in (self.lexer.LT_NAME, self.lexer.LT_STRING) and \
|
|
514
|
+
not (self.legacy and ltype == self.lexer.LT_NUMBER):
|
|
494
515
|
self._eprint(f's.rval only admits string literals and variables, but "{token}" is found.')
|
|
495
516
|
return None
|
|
496
517
|
|
|
@@ -596,17 +617,44 @@ class QuaTExParser:
|
|
|
596
617
|
def _parse_toplevel(self):
|
|
597
618
|
"""Parse the top level of a QuaTeX file"""
|
|
598
619
|
|
|
599
|
-
token
|
|
600
|
-
|
|
601
|
-
while not self.lexer.exhausted:
|
|
620
|
+
while token := self._next_token():
|
|
602
621
|
|
|
603
622
|
# Any top level statement starts with eval or other identifier
|
|
604
623
|
if self.lexer.ltype != self.lexer.LT_NAME:
|
|
605
624
|
self._eprint(f'unexpected token "{token}" at the top level.')
|
|
606
625
|
return False
|
|
607
626
|
|
|
627
|
+
# Import statement -- import "filename" ;
|
|
628
|
+
if token == 'import':
|
|
629
|
+
line, column = self.lexer.sline, self.lexer.scolumn
|
|
630
|
+
|
|
631
|
+
# Get the filename, it must be a string
|
|
632
|
+
path = self.lexer.get_token()
|
|
633
|
+
|
|
634
|
+
if self.lexer.ltype != self.lexer.LT_STRING:
|
|
635
|
+
self._eprint(f'unexpected token "{path}" where a string is required.')
|
|
636
|
+
return False
|
|
637
|
+
|
|
638
|
+
if not self._expect(';'):
|
|
639
|
+
return False
|
|
640
|
+
|
|
641
|
+
# Check whether the file exists
|
|
642
|
+
new_path = os.path.join(os.path.dirname(self.lexer.filename), path[1:-1])
|
|
643
|
+
|
|
644
|
+
if not os.path.exists(new_path):
|
|
645
|
+
self._eprint(f'cannot import {path}, file not found', line, column)
|
|
646
|
+
return False
|
|
647
|
+
|
|
648
|
+
# Jump to that file if it has not been processed yet
|
|
649
|
+
if (canonical_path := os.path.realpath(new_path)) not in self.seen_files:
|
|
650
|
+
self.seen_files.add(canonical_path)
|
|
651
|
+
|
|
652
|
+
new_file = open(new_path)
|
|
653
|
+
self.pending_lexers.append((self.lexer, new_file))
|
|
654
|
+
self.lexer = QuaTExLexer(new_file, new_path)
|
|
655
|
+
|
|
608
656
|
# Query -- eval E [ <expr> ] ;
|
|
609
|
-
|
|
657
|
+
elif token == 'eval':
|
|
610
658
|
# The query location is kept for future reference
|
|
611
659
|
line, column = self.lexer.sline, self.lexer.scolumn
|
|
612
660
|
|
|
@@ -638,10 +686,10 @@ class QuaTExParser:
|
|
|
638
686
|
|
|
639
687
|
# Ignore parameterized expressions with empty range
|
|
640
688
|
if parameter and parameter[1] > parameter[3]:
|
|
641
|
-
usermsgs.print_warning_loc(self.filename, line, column,
|
|
689
|
+
usermsgs.print_warning_loc(self.lexer.filename, line, column,
|
|
642
690
|
'ignoring parametric query with empty range.')
|
|
643
691
|
else:
|
|
644
|
-
self.queries.append((line, column, expr, parameter))
|
|
692
|
+
self.queries.append((self.lexer.filename, line, column, expr, parameter))
|
|
645
693
|
|
|
646
694
|
# Function definition -- <name> ( <args> ) = <expr> ;
|
|
647
695
|
else:
|
|
@@ -684,8 +732,6 @@ class QuaTExParser:
|
|
|
684
732
|
self.defs.append((fname, line, column, tuple(self.fvars), expr))
|
|
685
733
|
self.fvars.clear()
|
|
686
734
|
|
|
687
|
-
token = self.lexer.get_token()
|
|
688
|
-
|
|
689
735
|
return self.ok
|
|
690
736
|
|
|
691
737
|
def _check_tail(self, expr):
|
|
@@ -732,7 +778,7 @@ class QuaTExParser:
|
|
|
732
778
|
else:
|
|
733
779
|
arities[name] = len(args)
|
|
734
780
|
|
|
735
|
-
# Check whether all
|
|
781
|
+
# Check whether all calls are well-defined
|
|
736
782
|
for name, line, column, arity in self.calls:
|
|
737
783
|
def_arity = arities.get(name)
|
|
738
784
|
|
|
@@ -751,7 +797,7 @@ class QuaTExParser:
|
|
|
751
797
|
if not self._check_tail(expr):
|
|
752
798
|
ok = False
|
|
753
799
|
|
|
754
|
-
for
|
|
800
|
+
for _, _, _, expr, _ in self.queries:
|
|
755
801
|
if not self._check_tail(expr):
|
|
756
802
|
ok = False
|
|
757
803
|
|
|
@@ -782,26 +828,28 @@ class QuaTExParser:
|
|
|
782
828
|
line=line, column=column)
|
|
783
829
|
ok = False
|
|
784
830
|
|
|
785
|
-
for k, (line, column, expr, _) in enumerate(self.queries):
|
|
831
|
+
for k, (fname, line, column, expr, _) in enumerate(self.queries):
|
|
786
832
|
try:
|
|
787
833
|
expr = ast.Expression(expr)
|
|
788
834
|
ast.fix_missing_locations(expr)
|
|
789
|
-
slots[used_defs + k] = compile(expr, filename=f'query{line}:{column}', mode='eval')
|
|
835
|
+
slots[used_defs + k] = compile(expr, filename=f'query{fname}:{line}:{column}', mode='eval')
|
|
790
836
|
|
|
791
837
|
except TypeError:
|
|
792
838
|
self._eprint('this query cannot cannot be compiled.',
|
|
793
|
-
line=line, column=column)
|
|
839
|
+
line=line, column=column, fname=fname)
|
|
794
840
|
ok = False
|
|
795
841
|
|
|
796
842
|
if not ok:
|
|
797
843
|
return None
|
|
798
844
|
|
|
799
845
|
return QuaTExProgram(slots, varnames, len(self.fslots),
|
|
800
|
-
tuple((line, column, params) for line, column, _, params in self.queries))
|
|
846
|
+
tuple((fname, line, column, params) for fname, line, column, _, params in self.queries))
|
|
801
847
|
|
|
802
848
|
|
|
803
|
-
def parse_quatex(input_file, filename='<string>'):
|
|
849
|
+
def parse_quatex(input_file, filename='<string>', legacy=False):
|
|
804
850
|
"""Parse a QuaTEx formula"""
|
|
805
851
|
|
|
806
852
|
# Load, parse, and compile the QuaTEx file
|
|
807
|
-
|
|
853
|
+
parser = QuaTExParser(input_file, filename=filename, legacy=legacy)
|
|
854
|
+
|
|
855
|
+
return parser.parse(), parser.seen_files
|
umaudemc/simulators.py
CHANGED
umaudemc/statistical.py
CHANGED
|
@@ -133,9 +133,9 @@ def check_interval(qdata, num_sims, alpha, delta, quantile, verbose):
|
|
|
133
133
|
# Whether the size of the confidence interval for all queries have converged
|
|
134
134
|
converged = True
|
|
135
135
|
|
|
136
|
-
for
|
|
136
|
+
for query in qdata:
|
|
137
137
|
query.mu = query.sum / num_sims
|
|
138
|
-
query.s = math.sqrt((query.sum_sq - query.sum * query.mu) / (num_sims - 1))
|
|
138
|
+
query.s = math.sqrt(max(query.sum_sq - query.sum * query.mu, 0.0) / (num_sims - 1))
|
|
139
139
|
query.h = query.s * tinv
|
|
140
140
|
|
|
141
141
|
if query.h > delta:
|
|
@@ -285,7 +285,7 @@ def qdata_to_dict(num_sims, qdata, program):
|
|
|
285
285
|
qdata_it = iter(qdata)
|
|
286
286
|
q = next(qdata_it, None)
|
|
287
287
|
|
|
288
|
-
for k, (line, column, params) in enumerate(program.query_locations):
|
|
288
|
+
for k, (fname, line, column, params) in enumerate(program.query_locations):
|
|
289
289
|
# For parametric queries, we return an array of values
|
|
290
290
|
if params:
|
|
291
291
|
mean, std, radius = [], [], []
|
|
@@ -303,7 +303,7 @@ def qdata_to_dict(num_sims, qdata, program):
|
|
|
303
303
|
mean, std, radius = q.mu, q.s, q.h
|
|
304
304
|
param_info = {}
|
|
305
305
|
|
|
306
|
-
queries.append(dict(mean=mean, std=std, radius=radius, line=line, column=column, **param_info))
|
|
306
|
+
queries.append(dict(mean=mean, std=std, radius=radius, file=fname, line=line, column=column, **param_info))
|
|
307
307
|
|
|
308
308
|
return dict(nsims=num_sims, queries=queries)
|
|
309
309
|
|
|
@@ -326,7 +326,7 @@ def check(program, simulator, seed, alpha, delta, block, min_sim, max_sim, jobs,
|
|
|
326
326
|
# and the sum of their squares
|
|
327
327
|
qdata = [QueryData(k, idict)
|
|
328
328
|
for k, qinfo in enumerate(program.query_locations)
|
|
329
|
-
for idict in make_parameter_dicts(qinfo[
|
|
329
|
+
for idict in make_parameter_dicts(qinfo[3])]
|
|
330
330
|
|
|
331
331
|
# Run the simulations
|
|
332
332
|
if jobs == 1 and num_sims != 1:
|
umaudemc/usermsgs.py
CHANGED
|
@@ -23,6 +23,12 @@ def print_error_loc(unit, line, column, msg):
|
|
|
23
23
|
print(f'{tmn.bold}{unit}:{line}:{column}: {tmn.red}error:{tmn.reset} {msg}')
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def print_error_file(msg, unit):
|
|
27
|
+
"""Print an error with a location"""
|
|
28
|
+
|
|
29
|
+
print(f'{tmn.bold}{unit}: {tmn.red}error:{tmn.reset} {msg}')
|
|
30
|
+
|
|
31
|
+
|
|
26
32
|
def print_warning_loc(unit, line, column, msg):
|
|
27
33
|
"""Print a warning with a location"""
|
|
28
34
|
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: umaudemc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.1
|
|
4
4
|
Summary: Unified Maude model-checking utility
|
|
5
5
|
Author-email: ningit <ningit@users.noreply.github.com>
|
|
6
|
-
License:
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
7
|
Project-URL: Homepage, https://github.com/fadoss/umaudemc
|
|
8
8
|
Project-URL: Bug Tracker, https://github.com/fadoss/umaudemc/issues
|
|
9
9
|
Project-URL: Documentation, https://github.com/fadoss/umaudemc
|
|
10
10
|
Project-URL: Source Code, https://github.com/fadoss/umaudemc
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: Intended Audience :: Science/Research
|
|
13
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
14
13
|
Classifier: Topic :: Scientific/Engineering
|
|
15
14
|
Classifier: Operating System :: OS Independent
|
|
16
15
|
Requires-Python: >=3.9
|
|
@@ -25,6 +24,7 @@ Provides-Extra: plot
|
|
|
25
24
|
Requires-Dist: matplotlib; extra == "plot"
|
|
26
25
|
Provides-Extra: smc
|
|
27
26
|
Requires-Dist: scipy; extra == "smc"
|
|
27
|
+
Dynamic: license-file
|
|
28
28
|
|
|
29
29
|
# Unified Maude model-checking tool
|
|
30
30
|
|
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
umaudemc/__init__.py,sha256=
|
|
2
|
-
umaudemc/__main__.py,sha256=
|
|
1
|
+
umaudemc/__init__.py,sha256=8WhJAZouJDJEIY8zYgYIOv2VtMy_b_0q-sscUgsm7U0,23
|
|
2
|
+
umaudemc/__main__.py,sha256=x7HLEryX--lIKW1JYjF66XLZQ9lUnAQmd2ADGRipEfo,14823
|
|
3
3
|
umaudemc/api.py,sha256=I-o5foy8NUlO4JT4pX9L7kkuHQG_8_GMkWlOKt708E8,19733
|
|
4
4
|
umaudemc/backends.py,sha256=mzJkALYwcKPInT0lBiRsCxJSewKvx5j_akQsqWN1Ezo,4590
|
|
5
|
-
umaudemc/common.py,sha256=
|
|
5
|
+
umaudemc/common.py,sha256=UcIf7hTpP2qjcT9u_9-UcYR0nNeosx1xRZW7wsuT2bE,7305
|
|
6
6
|
umaudemc/counterprint.py,sha256=vVqM_UjGRk_xeftFxBGI5m6cQXV7mf8KvbQ_fvAvSQk,9226
|
|
7
|
+
umaudemc/distributed.py,sha256=2InONr9a4-n8lFVMWr57Hai3Rbuq6m4K-X4aDD1dYgE,8842
|
|
7
8
|
umaudemc/formatter.py,sha256=nbQlIsR5Xv18OEcpJdnTDGqO9xGL_amvBGFMU2OmheU,6026
|
|
8
9
|
umaudemc/formulae.py,sha256=jZPPDhjgsb7cs5rWvitiQoO0fd8JIlK98at2SN-LzVE,12156
|
|
9
10
|
umaudemc/grapher.py,sha256=K1chKNNlEzQvfOsiFmRPJmd9OpxRIrg6OyiMW6gqOCU,4348
|
|
10
11
|
umaudemc/gtk.py,sha256=61p4_OSFDfNHFD4lLz3QTZ7yZBra3RINmgbcnB-mUis,4249
|
|
11
12
|
umaudemc/jani.py,sha256=N5tE28jZC_OsI041nXOn02THlokpweATtEK-nx9pfWE,4130
|
|
12
|
-
umaudemc/kleene.py,sha256=
|
|
13
|
+
umaudemc/kleene.py,sha256=sW5SGdXpbLrjGtihPn8qgnhSH5WgltFaLVRx6GLwQU4,4697
|
|
13
14
|
umaudemc/mproc.py,sha256=9X5pTb3Z3XHcdOo8ynH7I5RZQpjzm9xr4IBbEtaglUE,11766
|
|
14
15
|
umaudemc/opsem.py,sha256=Xfdi9QGy-vcpmQ9ni8lBDAlKNw-fCRzYr6wnPbv6m1s,9448
|
|
15
16
|
umaudemc/probabilistic.py,sha256=MNvFeEd84-OYedSnyksZB87UckPfwizVNJepCItgRy8,29306
|
|
16
17
|
umaudemc/pyslang.py,sha256=zOfVGtfnOWDGghtaYLfQHq61KvbzVFmAM_0-upNhrTk,87753
|
|
17
|
-
umaudemc/quatex.py,sha256=
|
|
18
|
+
umaudemc/quatex.py,sha256=SQAbVz1csGXGqcfzFcjP89BdIpN8K2aiwP_PMLGPr1o,23239
|
|
18
19
|
umaudemc/resources.py,sha256=qKqvgLYTJVtsQHQMXFObyCLTo6-fssQeu_mG7tvVyD0,932
|
|
19
|
-
umaudemc/simulators.py,sha256=
|
|
20
|
-
umaudemc/statistical.py,sha256
|
|
20
|
+
umaudemc/simulators.py,sha256=Lk50Ql7hWUasWkQSWxboeR5LYfJtpwrANjUDuxYjuZ4,13232
|
|
21
|
+
umaudemc/statistical.py,sha256=buthWv4ovvxsvDs0eWgJw7lX2_9BsnLsW_PxW17RHCI,9087
|
|
21
22
|
umaudemc/terminal.py,sha256=B4GWLyW4Sdymgoavj418y4TI4MnWqNu3JS4BBoSYeTc,1037
|
|
22
|
-
umaudemc/usermsgs.py,sha256=
|
|
23
|
+
umaudemc/usermsgs.py,sha256=h4VPxljyKidEI8vpPcToKJA6mcLu9PtMkIh6vH3rDuA,719
|
|
23
24
|
umaudemc/webui.py,sha256=XlDV87tOOdcclHp2_oerrvHwRmCZdqAR4PppqeZm47A,11072
|
|
24
25
|
umaudemc/wrappers.py,sha256=uz9JV1zBVqzkuoByUd569fEcSxT_00aCJw-jcDtrFpE,9399
|
|
25
26
|
umaudemc/backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -39,8 +40,9 @@ umaudemc/command/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
|
|
|
39
40
|
umaudemc/command/check.py,sha256=PyaPDMw5OnPxSIZ10U4we0b5tTrjnYKAtAeQkJh2uLE,12031
|
|
40
41
|
umaudemc/command/graph.py,sha256=JqGzESC2sn-LBh2sqctrij03ItzwDO808s2qkNKUle0,6112
|
|
41
42
|
umaudemc/command/pcheck.py,sha256=eV4e4GcOHanP4hcIhMKd5Js22_ONac6kYj70FXun3mY,7274
|
|
42
|
-
umaudemc/command/scheck.py,sha256=
|
|
43
|
-
umaudemc/command/
|
|
43
|
+
umaudemc/command/scheck.py,sha256=jiVNsLfbNDUleWl9HuNW7GTQdszd5cefZJn0_Epm9UU,4967
|
|
44
|
+
umaudemc/command/sworker.py,sha256=0WzLoJBnjc5EYTuZK0fOQ5yoVhEBCH2ffm4WS8oM_yw,4383
|
|
45
|
+
umaudemc/command/test.py,sha256=Ru21JXNF61F5N5jayjwxp8okIjOAvuZuAlV_5ltQ-GU,37088
|
|
44
46
|
umaudemc/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
47
|
umaudemc/data/opsem.maude,sha256=geDP3_RMgtS1rRmYOybJDCXn_-dyHHxg0JxfYg1ftv0,27929
|
|
46
48
|
umaudemc/data/problog.maude,sha256=qvP90peT3J9gWi7I0x86jfrEXsVxDP5lcrUnSkTMhcY,3091
|
|
@@ -50,9 +52,9 @@ umaudemc/data/smcgraph.js,sha256=iCNQNmsuGdL_GLnqVhGDisediFtedxw3C24rxSiQwx8,667
|
|
|
50
52
|
umaudemc/data/smcview.css,sha256=ExFqrMkSeaf8VxFrJXflyCsRW3FTwbv78q0Hoo2UVrM,3833
|
|
51
53
|
umaudemc/data/smcview.js,sha256=_fHum1DRU1mhco-9-c6KqTLgiC5u_cCUf61jIK7wcIQ,14509
|
|
52
54
|
umaudemc/data/templog.maude,sha256=TZ-66hVWoG6gp7gJpS6FsQn7dpBTLrr76bKo-UfHGcA,9161
|
|
53
|
-
umaudemc-0.
|
|
54
|
-
umaudemc-0.
|
|
55
|
-
umaudemc-0.
|
|
56
|
-
umaudemc-0.
|
|
57
|
-
umaudemc-0.
|
|
58
|
-
umaudemc-0.
|
|
55
|
+
umaudemc-0.15.1.dist-info/licenses/LICENSE,sha256=MrEGL32oSWfnAZ0Bq4BZNcqnq3Mhp87Q4w6-deXfFnA,17992
|
|
56
|
+
umaudemc-0.15.1.dist-info/METADATA,sha256=zePwkmspjbYQuOOw0NfnjpS3XxFa-LvX5LH_azwapys,1654
|
|
57
|
+
umaudemc-0.15.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
58
|
+
umaudemc-0.15.1.dist-info/entry_points.txt,sha256=8rYRlLkn4orZtAoujDSeol1t_UFBrK0bfjmLTNv9B44,52
|
|
59
|
+
umaudemc-0.15.1.dist-info/top_level.txt,sha256=Yo_CF78HLGBSblk3890qLcx6XZ17zHCbGcT9iG8sfMw,9
|
|
60
|
+
umaudemc-0.15.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|