umaudemc 0.14.0__py3-none-any.whl → 0.15.0__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 CHANGED
@@ -1 +1 @@
1
- __version__ = '0.14.0'
1
+ __version__ = '0.15.0'
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
@@ -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} (line {line}:{column})')
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
- # Call the statistical model checker
174
- num_sims, qdata = check(program, simulator,
175
- args.seed, args.alpha, args.delta, args.block,
176
- min_sim, max_sim, args.jobs, args.verbose)
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,185 @@
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 socket
9
+ import sys
10
+ import tarfile
11
+ import tempfile
12
+ from array import array
13
+
14
+ from ..common import maude, parse_initial_data, usermsgs
15
+ from ..quatex import parse_quatex
16
+ from ..simulators import get_simulator
17
+ from ..statistical import run, QueryData, make_parameter_dicts
18
+
19
+ # Python-version-dependent option for safer TAR extraction
20
+ EXTRACT_OPTIONS = {'filter': 'data'} if sys.version_info >= (3, 12) else {}
21
+
22
+
23
+ def read_json(sock):
24
+ """Read a JSON value from a socket"""
25
+
26
+ # Read a 32 bit integer for the value length in bytes
27
+ if not (data := sock.recv(4)):
28
+ return None
29
+
30
+ length = int.from_bytes(data, 'big')
31
+
32
+ # Read the JSON to a byte string
33
+ data = b''
34
+
35
+ while len(data) < length:
36
+ data += sock.recv(length)
37
+
38
+ return json.loads(data.decode())
39
+
40
+
41
+ class Dummy:
42
+ """Dummy class to be used as a namespace"""
43
+
44
+ def __init__(self, values):
45
+ self.__dict__ = values
46
+
47
+
48
+ class Worker:
49
+ """Worker for simulations"""
50
+
51
+ def __init__(self):
52
+ self.program = None
53
+ self.simulator = None
54
+ self.block = 100
55
+
56
+ def setup(self, tmp_dir, args):
57
+ """Setup the execution environment"""
58
+
59
+ # Copy parameters
60
+ self.block = args.block
61
+
62
+ maude.setRandomSeed(args.seed)
63
+
64
+ # Do the same as the scheck command without checks
65
+ args.file = os.path.join(tmp_dir, args.file)
66
+
67
+ if not (data := parse_initial_data(args)):
68
+ return False
69
+
70
+ with open(os.path.join(tmp_dir, args.query)) as quatex_file:
71
+ self.program, _ = parse_quatex(quatex_file, filename=args.query,
72
+ legacy=args.assign == 'pmaude')
73
+
74
+ if not self.program:
75
+ return False
76
+
77
+ # Get the simulator for the given assignment method
78
+ self.simulator = get_simulator(args.assign, data)
79
+
80
+ if not self.simulator:
81
+ return False
82
+
83
+ return True
84
+
85
+ def run(self, conn):
86
+ """Run the simulation until it is finished"""
87
+
88
+ program = self.program
89
+ simulator = self.simulator
90
+ block = self.block
91
+
92
+ # Query data
93
+ qdata = [QueryData(k, idict)
94
+ for k, qinfo in enumerate(program.query_locations)
95
+ for idict in make_parameter_dicts(qinfo[3])]
96
+
97
+ sums = array('d', [0.0] * len(qdata))
98
+ sum_sq = array('d', [0.0] * len(qdata))
99
+
100
+ while True:
101
+
102
+ for _ in range(block):
103
+ # Run the simulation and compute all queries at once
104
+ values = run(program, qdata, simulator)
105
+
106
+ for k in range(len(qdata)):
107
+ sums[k] += values[k]
108
+ sum_sq[k] += values[k] * values[k]
109
+
110
+ conn.send(b'b' + sums.tobytes() + sum_sq.tobytes())
111
+
112
+ # Check whether to continue
113
+ answer = conn.recv(1)
114
+
115
+ if answer == b's':
116
+ print('Done')
117
+ return
118
+
119
+ elif answer != b'c':
120
+ usermsgs.print_error(f'Unknown command {answer.decode()}. Stopping.')
121
+ return
122
+
123
+ for k in range(len(qdata)):
124
+ sums[k] = 0
125
+ sum_sq[k] = 0
126
+
127
+
128
+ def handle_request(message, conn, addr, keep_file):
129
+ """Handle a request in a separate process"""
130
+
131
+ command = Dummy(message)
132
+
133
+ with tempfile.TemporaryDirectory(delete=not keep_file) as tmp_dir:
134
+ # Print the temporary working directory for debugging purposes
135
+ if keep_file:
136
+ print('Temporary directory:', tmp_dir)
137
+
138
+ # Recover the required files
139
+ with conn.makefile('rb', buffering=0) as fobj:
140
+ with tarfile.open(mode='r|*', fileobj=fobj) as tarf:
141
+ tarf.extractall(tmp_dir, **EXTRACT_OPTIONS)
142
+
143
+ # Setup a worker object
144
+ worker = Worker()
145
+
146
+ if not worker.setup(tmp_dir, command):
147
+ usermsgs.print_error(f'{addr}: bad request from scheck')
148
+ conn.send(b'e')
149
+ return
150
+
151
+ # Send confirmation
152
+ conn.send(b'o')
153
+
154
+ # Wait for start signal
155
+ conn.recv(1)
156
+
157
+ worker.run(conn)
158
+
159
+
160
+ def sworker(args):
161
+ """Worker for distributed statistical model checking"""
162
+
163
+ # Parse the listing address
164
+ try:
165
+ with socket.create_server((args.address, args.port), backlog=1) as sock:
166
+ while True:
167
+ print(f'👂 Listening on {args.address}:{args.port}...')
168
+ conn, addr = sock.accept()
169
+
170
+ usermsgs.print_info(f'Accepted connection from {":".join(map(str, addr))}.')
171
+
172
+ while True:
173
+ # Read the initiation message
174
+ if (message := read_json(conn)) is None:
175
+ break
176
+
177
+ # A separate process to cleanup Maude state
178
+ process = mp.Process(target=handle_request, args=(message, conn, addr, args.keep_file))
179
+ process.start()
180
+ process.join()
181
+
182
+ except KeyboardInterrupt:
183
+ print('Server closed by the user.')
184
+
185
+ 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
- extension = os.path.splitext(filename)[1]
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
- def find_maude_file(filename):
48
- """Find a Maude file taking MAUDE_LIB into account"""
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
- # Maude also considers the current working directory
53
- # and the directory of the Maude binary
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
- if maudelib is not None:
59
- paths += maudelib.split(os.pathsep)
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
- for path in paths:
62
- abspath = os.path.join(path, filename)
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
- return None
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
@@ -0,0 +1,322 @@
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
+ # Normalize workers to a dictionary
130
+ workers = dspec.get('workers')
131
+
132
+ if not isinstance(workers, list):
133
+ usermsgs.print_error_file('the distribution specification does not contain a list-valued \'workers\' key.', fname)
134
+ return False
135
+
136
+ for k, worker in enumerate(workers):
137
+ # Strings address:port are allowed
138
+ if isinstance(worker, str):
139
+ try:
140
+ address, port = worker.split(':')
141
+ worker = {'address': address, 'port': int(port)}
142
+
143
+ except ValueError:
144
+ usermsgs.print_error_file(f'bad address specification {worker} for worker {k + 1} '
145
+ '(it should be <address>:<port>).', fname)
146
+ return False
147
+
148
+ workers[k] = worker
149
+
150
+ # Otherwise, it must be a dictionary
151
+ elif not isinstance(worker, dict):
152
+ usermsgs.print_error_file(f'the specification for worker {k + 1} is not a dictionary.', fname)
153
+ return False
154
+
155
+ # With address and port keys
156
+ else:
157
+ for key, ktype in (('address', str), ('port', int)):
158
+ if key not in worker:
159
+ usermsgs.print_error_file(f'missing key \'{key}\' for worker {k + 1}.', fname)
160
+ return False
161
+
162
+ if not isinstance(worker[key], ktype):
163
+ usermsgs.print_error_file(f'wrong type for key \'{key}\' in worker {k + 1}, {ktype.__name__} expected.', fname)
164
+ return False
165
+
166
+ # Name just for reference in errors and messages
167
+ if 'name' not in worker:
168
+ worker['name'] = f'{worker["address"]}:{worker["port"]}'
169
+
170
+ return True
171
+
172
+
173
+ def setup_workers(args, initial_data, dspec, seen_files, stack):
174
+ """Setup workers and send problem data"""
175
+
176
+ workers = dspec['workers']
177
+
178
+ # Generate a random seed for each worker
179
+ seeds = [random.getrandbits(20) for _ in range(len(workers))]
180
+
181
+ # Data to be passed to the external machine
182
+ COPY = ('initial', 'strategy', 'module', 'metamodule', 'opaque', 'full_matchrew',
183
+ 'purge_fails', 'merge_states', 'assign', 'block', 'query', 'assign', 'advise', 'verbose')
184
+
185
+ data = {key: args.__dict__[key] for key in COPY} | {'file': 'source.maude'}
186
+
187
+ # Make a flattened version of the Maude file
188
+ flat_source = io.BytesIO()
189
+ flatten_maude_file(initial_data.filename, flat_source)
190
+
191
+ flat_info = tarfile.TarInfo('source.maude')
192
+ flat_info.size = flat_source.getbuffer().nbytes
193
+
194
+ # Save the sockets for each worker
195
+ sockets = []
196
+
197
+ for worker, seed in zip(workers, seeds):
198
+ address, port = worker['address'], worker['port']
199
+
200
+ try:
201
+ sock = socket.create_connection((address, int(port)))
202
+
203
+ except ConnectionRefusedError:
204
+ usermsgs.print_error(f'Connection refused by worker \'{worker["name"]}\'.')
205
+ return None
206
+
207
+ stack.enter_context(sock)
208
+ sockets.append(sock)
209
+
210
+ # Send the input data
211
+ input_data = data | {'seed': seed}
212
+
213
+ if block_size := worker.get('block'):
214
+ input_data['block'] = block_size # if specified
215
+
216
+ input_data = json.dumps(input_data).encode()
217
+ sock.sendall(len(input_data).to_bytes(4) + input_data)
218
+
219
+ # Send the relevant files
220
+ with sock.makefile('wb', buffering=0) as fobj:
221
+ with tarfile.open(mode='w|gz', fileobj=fobj) as tarf:
222
+ flat_source.seek(0)
223
+ tarf.addfile(flat_info, flat_source)
224
+
225
+ for file in seen_files:
226
+ relpath = os.path.relpath(file)
227
+
228
+ if relpath.startswith('..'):
229
+ usermsgs.print_error('QuaTEx file outside the working tree, it will not be included and the execution will fail.')
230
+ else:
231
+ tarf.add(relpath)
232
+
233
+ fobj.flush()
234
+
235
+ # Receive confirmation from the remote
236
+ answer = sock.recv(1)
237
+
238
+ if answer != b'o':
239
+ usermsgs.print_error(f'Configuration error in {worker["name"]} worker.')
240
+ return None
241
+
242
+ return sockets
243
+
244
+
245
+ def distributed_check(args, initial_data, min_sim, max_sim, program, seen_files):
246
+ """Distributed statistical model checking"""
247
+
248
+ # Load the distribution specification
249
+ if (dspec := load_specification(args.distribute, 'distribution specification')) is None \
250
+ or not process_dspec(dspec, args.distribute):
251
+ return None, None
252
+
253
+ # Gather all sockets in a context to close them when we finish
254
+ with ExitStack() as stack:
255
+
256
+ # Socket to connect with the workers
257
+ if not (sockets := setup_workers(args, initial_data, dspec, seen_files, stack)):
258
+ return None, None
259
+
260
+ print('All workers are ready. Starting...')
261
+
262
+ # Use a selector to wait for updates from any worker
263
+ selector = selectors.DefaultSelector()
264
+
265
+ for sock, data in zip(sockets, dspec['workers']):
266
+ selector.register(sock, selectors.EVENT_READ, data=data)
267
+ sock.send(b'c')
268
+
269
+ buffer = array('d')
270
+
271
+ # Query data
272
+ qdata = [QueryData(k, idict)
273
+ for k, qinfo in enumerate(program.query_locations)
274
+ for idict in make_parameter_dicts(qinfo[3])]
275
+ nqueries = len(qdata)
276
+ num_sims = 0
277
+
278
+ quantile = get_quantile_func()
279
+
280
+ while sockets:
281
+ events = selector.select()
282
+ finished = []
283
+
284
+ for key, _ in events:
285
+ sock = key.fileobj
286
+
287
+ answer = sock.recv(1)
288
+
289
+ if answer == b'b':
290
+ data = sock.recv(16 * nqueries)
291
+ buffer.frombytes(data)
292
+
293
+ for k in range(nqueries):
294
+ qdata[k].sum += buffer[k]
295
+ qdata[k].sum_sq += buffer[nqueries + k]
296
+
297
+ num_sims += key.data['block']
298
+
299
+ del buffer[:]
300
+ finished.append(key.fileobj)
301
+
302
+ else:
303
+ usermsgs.print_error(f'Server {key.data["name"]} disconnected or misbehaving')
304
+ selector.unregister(key.fileobj)
305
+ sockets.remove(key.fileobj)
306
+
307
+ # Check whether the simulation has converged
308
+ converged = check_interval(qdata, num_sims, args.alpha, args.delta, quantile, args.verbose)
309
+
310
+ if converged or max_sim and num_sims >= max_sim:
311
+ break
312
+
313
+ for sock in finished:
314
+ sock.send(b'c')
315
+
316
+ finished.clear()
317
+
318
+ # Send stop signal to all workers
319
+ for sock in sockets:
320
+ sock.send(b's')
321
+
322
+ 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 k, state in enumerate(self.state_list):
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.filename = filename
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.reval
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, self.lexer.LT_NUMBER):
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 = self.lexer.get_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
- if token == 'eval':
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 called are well-defined
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 line, column, expr, _ in self.queries:
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
- return QuaTExParser(input_file, filename=filename).parse()
853
+ parser = QuaTExParser(input_file, filename=filename, legacy=legacy)
854
+
855
+ return parser.parse(), parser.seen_files
umaudemc/simulators.py CHANGED
@@ -218,7 +218,7 @@ class StrategyDTMCSimulator(BaseSimulator):
218
218
  p = sc.compile(ml.upStrategy(strategy))
219
219
 
220
220
  try:
221
- self.graph = MarkovRunner(p, initial).run()
221
+ self.graph, _ = MarkovRunner(p, initial).run()
222
222
  self.node = self.graph
223
223
  self.time = 0.0
224
224
 
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 k, query in enumerate(qdata):
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[2])]
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
1
+ Metadata-Version: 2.4
2
2
  Name: umaudemc
3
- Version: 0.14.0
3
+ Version: 0.15.0
4
4
  Summary: Unified Maude model-checking utility
5
5
  Author-email: ningit <ningit@users.noreply.github.com>
6
- License: GPLv3
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=4pkGIcN2HdoT4OEADiR3LUoE-VfT0mtYYKhwVWEwVMY,23
2
- umaudemc/__main__.py,sha256=PBIa2mStJwNIK5YnwoCefEB_a59VLkHE_Zp_1hnMyg4,14060
1
+ umaudemc/__init__.py,sha256=iCWCR3zceWVR3HY2_dnmoKBKY7eAZrsQ5pfmt6srIVw,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=Z1RQNwNpwHApsckHM4Zj5A0ClTuVKKKC1Lkgf9yYmqI,4926
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=6iR02PYQBZOWZ3bIm7AP-sCSFdTazXHb25FqldUxIg8,8653
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=Yxo9O2rjJNpS6y4Qfb_SP71tDrryV0KKUfmBIKXypwg,4458
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=sSgGXhHmu_7XqBvcEv7EtGeecZ704OB3B8NFk8XXD00,21600
18
+ umaudemc/quatex.py,sha256=SQAbVz1csGXGqcfzFcjP89BdIpN8K2aiwP_PMLGPr1o,23239
18
19
  umaudemc/resources.py,sha256=qKqvgLYTJVtsQHQMXFObyCLTo6-fssQeu_mG7tvVyD0,932
19
- umaudemc/simulators.py,sha256=84W30MWWe4QRxK24RGA0zuwXjPya4wH18PrhPKdOpyU,13229
20
- umaudemc/statistical.py,sha256=-UzP3g4Sy5L_dq2UEimobwu1qR70uEQnqKigsPEvyzU,9074
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=d3RfyBGEBmcV_c2MeXWCtZIiiM2vFnaHsN3MHwMnyAs,583
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=eRGE-2SsOf6etIfJQ9dJe4cSch8qAs7IuOR94i-E1_U,4653
43
- umaudemc/command/test.py,sha256=kPmV1-hIJMMxZLErqYYP50z95uME_3sLLUMXQuUy9fs,38733
43
+ umaudemc/command/scheck.py,sha256=jiVNsLfbNDUleWl9HuNW7GTQdszd5cefZJn0_Epm9UU,4967
44
+ umaudemc/command/sworker.py,sha256=hZv7hpZg3Z-BnTtYqNwG7y2Njyr3NMV7-EIANrj8POM,4344
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.14.0.dist-info/LICENSE,sha256=MrEGL32oSWfnAZ0Bq4BZNcqnq3Mhp87Q4w6-deXfFnA,17992
54
- umaudemc-0.14.0.dist-info/METADATA,sha256=nQh_n1_3pCq1rF3X1l0Ps5DIt9hlSqgd5tCAwrYW9sI,1687
55
- umaudemc-0.14.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
56
- umaudemc-0.14.0.dist-info/entry_points.txt,sha256=8rYRlLkn4orZtAoujDSeol1t_UFBrK0bfjmLTNv9B44,52
57
- umaudemc-0.14.0.dist-info/top_level.txt,sha256=Yo_CF78HLGBSblk3890qLcx6XZ17zHCbGcT9iG8sfMw,9
58
- umaudemc-0.14.0.dist-info/RECORD,,
55
+ umaudemc-0.15.0.dist-info/licenses/LICENSE,sha256=MrEGL32oSWfnAZ0Bq4BZNcqnq3Mhp87Q4w6-deXfFnA,17992
56
+ umaudemc-0.15.0.dist-info/METADATA,sha256=MQLEOo16TB5BJRq90duHk0UTEMVF8i8MeENz7jt1OdE,1654
57
+ umaudemc-0.15.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
+ umaudemc-0.15.0.dist-info/entry_points.txt,sha256=8rYRlLkn4orZtAoujDSeol1t_UFBrK0bfjmLTNv9B44,52
59
+ umaudemc-0.15.0.dist-info/top_level.txt,sha256=Yo_CF78HLGBSblk3890qLcx6XZ17zHCbGcT9iG8sfMw,9
60
+ umaudemc-0.15.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5