umaudemc 0.15.1__py3-none-any.whl → 0.17.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/quatex.py CHANGED
@@ -8,6 +8,19 @@ import os
8
8
  from . import usermsgs
9
9
 
10
10
 
11
+ class QuaTExQuery:
12
+ """QuaTeX query information"""
13
+
14
+ def __init__(self, fname, line, column, expr, parameters, delta):
15
+ self.filename = fname # file cointing the query
16
+ self.line = line # query location
17
+ self.column = column
18
+ self.parameters = parameters # parameters as (variable, start, step, end)
19
+
20
+ self.expr = expr # query expression (only needed for compilation)
21
+ self.delta = delta # delta value
22
+
23
+
11
24
  class QuaTExProgram:
12
25
  """Compiled QuaTEx program"""
13
26
 
@@ -23,7 +36,7 @@ class QuaTExProgram:
23
36
  self.nqueries = len(slots) - ndefs
24
37
 
25
38
  # Query information (file name, line, column, and parameters)
26
- self.query_locations = qinfo
39
+ self.queries = qinfo
27
40
 
28
41
 
29
42
  class QuaTExLexer:
@@ -144,6 +157,11 @@ class QuaTExLexer:
144
157
  self.sline = self.line
145
158
  self.scolumn = self.column
146
159
 
160
+ @staticmethod
161
+ def _is_name(c):
162
+ """Whether the character is allowed in a name"""
163
+ return c.isalnum() or c == '$'
164
+
147
165
  def get_token(self):
148
166
  """Get the next token from the stream"""
149
167
 
@@ -166,9 +184,10 @@ class QuaTExLexer:
166
184
  self.ltype = self.LT_STRING
167
185
  self._capture_string()
168
186
 
169
- elif c.isalpha():
187
+ # Names cannot start with a number
188
+ elif c.isalpha() or c == '$':
170
189
  self.ltype = self.LT_NAME
171
- self._capture(str.isalnum)
190
+ self._capture(self._is_name)
172
191
 
173
192
  elif c.isdecimal():
174
193
  self.ltype = self.LT_NUMBER
@@ -206,9 +225,9 @@ class QuaTExLexer:
206
225
  class QuaTExParser:
207
226
  """Parser for QuaTEx"""
208
227
 
209
- PS_IFC = 0 # condition
210
- PS_IFT = 1 # true branch
211
- PS_IFF = 2 # negative branch
228
+ PS_IF_COND = 0 # condition
229
+ PS_IF_THEN = 1 # true branch
230
+ PS_IF_ELSE = 2 # negative branch
212
231
  PS_PAREN = 3 # parenthesis
213
232
  PS_ARITH = 4 # completing an arithmetic expression
214
233
  PS_CALLARGS = 5 # call arguments
@@ -218,12 +237,13 @@ class QuaTExParser:
218
237
  BINOPS_PREC = (4, 4, 3, 3, 3, 11, 12, 7, 7, 6, 6, 6, 6)
219
238
  BINOPS_AST = (ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod, ast.And, ast.Or, ast.Eq,
220
239
  ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE)
240
+ # Kind of binary operator (0 = BinOp, 1 = BoolOp, 2 = Compare)
221
241
  BINOPS_CMP = (0, ) * 5 + (1, ) * 2 + (2, ) * 6
222
242
  # Unary operator and its precedence (as in C)
223
243
  UNARY_OPS = ('!', )
224
244
  UNARY_AST = (ast.Not, )
225
245
 
226
- def __init__(self, source, filename='<stdin>', legacy=False):
246
+ def __init__(self, source, filename='<stdin>', legacy=False, constants=None):
227
247
  # Filename is only used for diagnostics
228
248
  self.lexer = QuaTExLexer(source, filename)
229
249
  # PMaude legacy syntax
@@ -233,11 +253,14 @@ class QuaTExParser:
233
253
  self.pending_lexers = []
234
254
  self.seen_files = set() if filename.startswith('<') else {os.path.realpath(filename)}
235
255
 
236
- # Parameters of the current function
237
- self.fvars = []
238
- # Whether the variables that may occur
239
- # in an expression are known
256
+ # Whether the variables that may occur in an expression are known
240
257
  self.known_vars = True
258
+ # Parameters of the current function if self.known_vars
259
+ # or variables found in the last processed expression otherwise
260
+ self.fvars = []
261
+ # Constants defined outside
262
+ self.constants = {} if constants is None else constants
263
+ self.pending_constants = []
241
264
 
242
265
  # State stack for parsing expressions
243
266
  self.stack = []
@@ -246,7 +269,7 @@ class QuaTExParser:
246
269
  # Whether parsing errors have been encountered
247
270
  self.ok = True
248
271
 
249
- # Compilation slot indices for each number
272
+ # Compilation slot indices for each element
250
273
  self.fslots = {}
251
274
  self.calls = []
252
275
  self.observations = []
@@ -277,13 +300,29 @@ class QuaTExParser:
277
300
 
278
301
  return True
279
302
 
303
+ def _expect_any(self, *options):
304
+ """Check whether any of the given strings is the next token"""
305
+
306
+ atoken = self.lexer.get_token()
307
+ options_str = ', '.join(f'"{opt}"' for opt in options)
308
+
309
+ if atoken is None:
310
+ self._eprint(f'unexpected end of file where any of {options_str} is required.')
311
+ return None
312
+
313
+ if atoken not in options:
314
+ self._eprint(f'unexpected token "{atoken}" where any of {options_str} is required.')
315
+ return None
316
+
317
+ return atoken
318
+
280
319
  def _in_state(self, state):
281
320
  """Check whether the parser is in the given state"""
282
321
 
283
322
  return self.stack and self.stack[-1] == state
284
323
 
285
324
  def _next_token(self):
286
- """Get the next token without potential file exhaustion"""
325
+ """Get the next token dealing with file exhaustion"""
287
326
 
288
327
  token = self.lexer.get_token()
289
328
 
@@ -325,40 +364,89 @@ class QuaTExParser:
325
364
  self._eprint(f'unexpected token "{var_name}" where a variable name is required.')
326
365
  return (None, ) * 3
327
366
 
367
+ if not self._expect(','):
368
+ return (None, ) * 3
369
+
328
370
  # Parameter specification (name, initial value, step, last value)
329
371
  spec = [var_name]
330
372
 
331
- # Parse the initial value, step, and last value
332
- for _ in range(3):
333
- if not self._expect(','):
334
- return (None, ) * 3
373
+ # Make a copy of the expression variables, since we continue in
374
+ # known variables mode with no variables allowed
375
+ expr_vars = tuple(self.fvars)
376
+ self.fvars.clear()
335
377
 
336
- token = self.lexer.get_token()
378
+ # Parse the initial value, step, and last value
379
+ for k in range(3):
380
+ expr = self._parse_constexpr(',' if k < 2 else ')')
337
381
 
338
- if self.lexer.ltype != self.lexer.LT_NUMBER:
339
- self._eprint(f'unexpected token "{token}" where a number is required.')
382
+ if expr is None:
340
383
  return (None, ) * 3
341
384
 
342
- spec.append(float(token))
343
-
344
- # Check the closing parenthesis
345
- if not self._expect(')'):
346
- return (None, ) * 3
385
+ spec.append(float(expr))
347
386
 
348
387
  # Check whether the variables in the expressions are the parameter
349
- for var, line, column in self.fvars:
388
+ for var, line, column in expr_vars:
350
389
  if var != var_name:
351
390
  self._eprint(f'unknown variable "{var}".', line=line, column=column)
352
391
  self.ok = False
353
392
 
393
+ return tuple(spec)
394
+
395
+ def _parse_delta(self, parameter):
396
+ """Parse the delta annotation"""
397
+
398
+ if not self._expect('delta', '='):
399
+ return None
400
+
401
+ # Only the parameter can appear as free variable in the expression
354
402
  self.fvars.clear()
355
403
 
356
- return tuple(spec)
404
+ if parameter:
405
+ self.fvars.append(parameter[0])
406
+
407
+ if (delta := self._parse_expr(';', constexpr=True)) is None:
408
+ return None
409
+
410
+ # Prepare delta as a function on the parameter (if any)
411
+ args = [ast.arg(parameter[0])] if parameter else []
412
+
413
+ delta_fn = ast.Expression(ast.Lambda(ast.arguments(args=args), delta))
414
+ ast.fix_missing_locations(delta_fn)
357
415
 
358
- def _parse_expr(self, end_token, inside_def=False):
416
+ return eval(compile(delta_fn, '<delta>', 'eval'), {}, {})
417
+
418
+ def _parse_constexpr(self, end_token):
419
+ """Parse a constant expression as a number"""
420
+
421
+ # Soft errors, like unbound variables, are hard here because
422
+ # we run the code to obtain a value for the expression
423
+ old_ok, self.ok = self.ok, True
424
+
425
+ if (expr := self._parse_expr(end_token, constexpr=True)) is None:
426
+ return None
427
+
428
+ elif not self.ok:
429
+ return 0.0 # dummy value to keep running
430
+
431
+ self.ok = old_ok
432
+
433
+ # Evaluate the expression
434
+ expr = ast.Expression(expr)
435
+ ast.fix_missing_locations(expr)
436
+
437
+ result = eval(compile(expr, self.lexer.filename, 'eval'), {}, {})
438
+
439
+ # Check whether it is a number
440
+ if not isinstance(result, (int, float)):
441
+ self._eprint(f'"{result}" obtained while a number is expected.')
442
+ return None
443
+
444
+ return result
445
+
446
+ def _parse_expr(self, end_token, constexpr=False):
359
447
  """Parse an expression"""
360
448
 
361
- # Current expression
449
+ # Current expression (as a Python AST node)
362
450
  current = None
363
451
  # Number of nested conditions in the current position
364
452
  inside_cond = 0
@@ -393,13 +481,13 @@ class QuaTExParser:
393
481
  self._eprint('misplaced "if" keyword.')
394
482
  return None
395
483
 
396
- self.stack.append(self.PS_IFC)
484
+ self.stack.append(self.PS_IF_COND)
397
485
  arg_stack.append([])
398
486
  inside_cond += 1
399
487
 
400
488
  elif token == 'then':
401
- if current and self._in_state(self.PS_IFC):
402
- self.stack[-1] = self.PS_IFT
489
+ if current and self._in_state(self.PS_IF_COND):
490
+ self.stack[-1] = self.PS_IF_THEN
403
491
  arg_stack[-1].append(current)
404
492
  current = None
405
493
  inside_cond -= 1
@@ -408,8 +496,8 @@ class QuaTExParser:
408
496
  return None
409
497
 
410
498
  elif token == 'else':
411
- if current and self._in_state(self.PS_IFT):
412
- self.stack[-1] = self.PS_IFF
499
+ if current and self._in_state(self.PS_IF_THEN):
500
+ self.stack[-1] = self.PS_IF_ELSE
413
501
  arg_stack[-1].append(current)
414
502
  current = None
415
503
  else:
@@ -417,7 +505,7 @@ class QuaTExParser:
417
505
  return None
418
506
 
419
507
  elif token == 'fi':
420
- if current and self._in_state(self.PS_IFF):
508
+ if current and self._in_state(self.PS_IF_ELSE):
421
509
  arg_stack[-1].append(current)
422
510
  current = ast.IfExp(*arg_stack[-1])
423
511
  arg_stack.pop()
@@ -435,6 +523,10 @@ class QuaTExParser:
435
523
  self._eprint('the next operator # cannot be used in conditions or call arguments.')
436
524
  self.ok = False
437
525
 
526
+ elif constexpr:
527
+ self._eprint('the next operator # cannot be used in constant contexts.')
528
+ self.ok = False
529
+
438
530
  # A function call should follow
439
531
  token = self.lexer.get_token()
440
532
  line, column = self.lexer.sline, self.lexer.scolumn
@@ -448,6 +540,14 @@ class QuaTExParser:
448
540
  inside_next = True
449
541
  call_name, call_line, call_column = token, line, column
450
542
 
543
+ # discard is a soft keyword
544
+ elif token == 'discard' and not (inside_cond or call_name or constexpr):
545
+ if current:
546
+ self._eprint('misplaced discard keyword.')
547
+ return None
548
+
549
+ current = ast.Constant(None)
550
+
451
551
  elif token == ',':
452
552
  if current and self._in_state(self.PS_CALLARGS):
453
553
  arg_stack[-1].append(current)
@@ -477,10 +577,15 @@ class QuaTExParser:
477
577
  self._eprint(f'argument missing after a comma in a call to "{call_name}".')
478
578
  self.ok = False
479
579
 
580
+ # Call are internally represented as tuples that indicate whether
581
+ # the call is preceded by next (i.e. to be evaluated after a step)
582
+ # and which code slot contains the callee definition
480
583
  slot = self.fslots.setdefault(call_name, len(self.fslots))
481
584
  current = ast.Tuple([ast.Constant(inside_next), ast.Constant(slot), *args], ast.Load())
482
- current.custom_loc = (call_line, call_column)
483
- self.calls.append((call_name, call_line, call_column, len(args)))
585
+ # We keep the source location for error reporting (using the attributes
586
+ # lineno and col_offset of the AST node only here is not possible)
587
+ current.custom_loc = dict(fname=self.lexer.filename, line=call_line, column=call_column)
588
+ self.calls.append((call_name, self.lexer.filename, call_line, call_column, len(args)))
484
589
  inside_next = False
485
590
  call_name = None
486
591
 
@@ -535,6 +640,10 @@ class QuaTExParser:
535
640
  self._eprint(f'"{token}" is called in a condition or call argument, but this is not allowed.')
536
641
  return None
537
642
 
643
+ if constexpr:
644
+ self._eprint(f'"{token}" is called in a constant context, but calls are not allowed.')
645
+ return None
646
+
538
647
  self.stack.append(self.PS_CALLARGS)
539
648
  arg_stack.append([])
540
649
  call_name = token
@@ -542,14 +651,19 @@ class QuaTExParser:
542
651
 
543
652
  # Simply a variable
544
653
  else:
545
- if not self.known_vars:
654
+ current = ast.Name(token, ast.Load())
655
+
656
+ # Constants (from the command line) start with $, but variables can also
657
+ if token.startswith('$') and (value := self.constants.get(token[1:])) is not None:
658
+ current = ast.Constant(value)
659
+
660
+ elif not self.known_vars:
546
661
  self.fvars.append((token, line, column))
547
662
 
548
663
  elif token not in self.fvars:
549
664
  self._eprint(f'unknown variable "{token}".', line=line, column=column)
550
665
  self.ok = False
551
666
 
552
- current = ast.Name(token, ast.Load())
553
667
 
554
668
  # We continue with the peeked token
555
669
  token = next_token
@@ -674,22 +788,29 @@ class QuaTExParser:
674
788
  if parameter:
675
789
  self.known_vars = False
676
790
 
677
- expr = self._parse_expr(']')
791
+ if not (expr := self._parse_expr(']')):
792
+ return False
678
793
 
679
794
  self.known_vars = True
680
795
 
681
796
  # Parse parameter specification in parametric queries
682
797
  parameter = self._parse_parameter() if parameter else None
798
+ delta = None
683
799
 
684
- if not expr or not self._expect(';'):
800
+ # Check whether there is a "with delta = <number>" prefix
801
+ if (token := self._expect_any('with', ';')) is None:
685
802
  return False
686
803
 
804
+ if token == 'with':
805
+ if (delta := self._parse_delta(parameter)) is None:
806
+ return False
807
+
687
808
  # Ignore parameterized expressions with empty range
688
809
  if parameter and parameter[1] > parameter[3]:
689
810
  usermsgs.print_warning_loc(self.lexer.filename, line, column,
690
811
  'ignoring parametric query with empty range.')
691
812
  else:
692
- self.queries.append((self.lexer.filename, line, column, expr, parameter))
813
+ self.queries.append(QuaTExQuery(self.lexer.filename, line, column, expr, parameter, delta))
693
814
 
694
815
  # Function definition -- <name> ( <args> ) = <expr> ;
695
816
  else:
@@ -724,7 +845,7 @@ class QuaTExParser:
724
845
  if not self._expect('='):
725
846
  return False
726
847
 
727
- expr = self._parse_expr(';', inside_def=True)
848
+ expr = self._parse_expr(';')
728
849
 
729
850
  if not expr:
730
851
  return False
@@ -743,8 +864,7 @@ class QuaTExParser:
743
864
  expr, tail_pos = pending.pop()
744
865
 
745
866
  if isinstance(expr, ast.Tuple) and not tail_pos:
746
- self._eprint('non-tail calls are not allowed.',
747
- line=expr.custom_loc[0], column=expr.custom_loc[1])
867
+ self._eprint('non-tail calls are not allowed.', **expr.custom_loc)
748
868
  return False
749
869
 
750
870
  elif isinstance(expr, ast.UnaryOp):
@@ -779,17 +899,17 @@ class QuaTExParser:
779
899
  arities[name] = len(args)
780
900
 
781
901
  # Check whether all calls are well-defined
782
- for name, line, column, arity in self.calls:
902
+ for name, fname, line, column, arity in self.calls:
783
903
  def_arity = arities.get(name)
784
904
 
785
905
  if def_arity is None:
786
906
  self._eprint(f'call to undefined function "{name}".',
787
- line=line, column=column)
907
+ line=line, column=column, fname=fname)
788
908
  ok = False
789
909
 
790
910
  elif arity != def_arity:
791
911
  self._eprint(f'wrong number of arguments in a call to "{name}" ({arity} given, but {def_arity} expected).',
792
- line=line, column=column)
912
+ line=line, column=column, fname=fname)
793
913
  ok = False
794
914
 
795
915
  # Check all calls are tail in expression
@@ -797,8 +917,8 @@ class QuaTExParser:
797
917
  if not self._check_tail(expr):
798
918
  ok = False
799
919
 
800
- for _, _, _, expr, _ in self.queries:
801
- if not self._check_tail(expr):
920
+ for query in self.queries:
921
+ if not self._check_tail(query.expr):
802
922
  ok = False
803
923
 
804
924
  if not ok:
@@ -828,28 +948,30 @@ class QuaTExParser:
828
948
  line=line, column=column)
829
949
  ok = False
830
950
 
831
- for k, (fname, line, column, expr, _) in enumerate(self.queries):
951
+ for k, query in enumerate(self.queries):
832
952
  try:
833
- expr = ast.Expression(expr)
953
+ expr = ast.Expression(query.expr)
834
954
  ast.fix_missing_locations(expr)
835
- slots[used_defs + k] = compile(expr, filename=f'query{fname}:{line}:{column}', mode='eval')
955
+ slots[used_defs + k] = compile(expr, filename=f'query{query.filename}:{query.line}:{query.column}', mode='eval')
836
956
 
837
957
  except TypeError:
838
958
  self._eprint('this query cannot cannot be compiled.',
839
- line=line, column=column, fname=fname)
959
+ line=query.line, column=query.column, fname=query.fname)
840
960
  ok = False
841
961
 
962
+ # No longer needed
963
+ query.expr = None
964
+
842
965
  if not ok:
843
966
  return None
844
967
 
845
- return QuaTExProgram(slots, varnames, len(self.fslots),
846
- tuple((fname, line, column, params) for fname, line, column, _, params in self.queries))
968
+ return QuaTExProgram(slots, varnames, len(self.fslots), self.queries)
847
969
 
848
970
 
849
- def parse_quatex(input_file, filename='<string>', legacy=False):
971
+ def parse_quatex(input_file, filename='<string>', legacy=False, constants=None):
850
972
  """Parse a QuaTEx formula"""
851
973
 
852
974
  # Load, parse, and compile the QuaTEx file
853
- parser = QuaTExParser(input_file, filename=filename, legacy=legacy)
975
+ parser = QuaTExParser(input_file, filename=filename, legacy=legacy, constants=constants)
854
976
 
855
977
  return parser.parse(), parser.seen_files
umaudemc/simulators.py CHANGED
@@ -27,7 +27,11 @@ def parse_hole_term(module, term_str):
27
27
 
28
28
  # Collect all variables in the term
29
29
  varset = set()
30
- collect_vars(term, varset)
30
+
31
+ if term.isVariable():
32
+ varset.add(term)
33
+ else:
34
+ collect_vars(term, varset)
31
35
 
32
36
  if len(varset) > 1:
33
37
  usermsgs.print_warning('The observation "{message}" '
@@ -105,6 +109,42 @@ class StrategyStepSimulator(BaseSimulator):
105
109
  self.step += 1
106
110
 
107
111
 
112
+ class RuleStepSimulator(BaseSimulator):
113
+ """Simulator where rule application (potentially using random) is the step"""
114
+
115
+ def __init__(self, initial):
116
+ super().__init__(initial)
117
+
118
+ # See the PMaude simulator to an explanation for why we need that
119
+ self.random = None
120
+
121
+ if nat_kind := self.module.findSort('Nat').kind():
122
+ if random := self.module.findSymbol('random', (nat_kind,), nat_kind):
123
+ self.random = random(self.module.parseTerm('1', nat_kind))
124
+
125
+ def restart(self):
126
+ """Restart simulator"""
127
+
128
+ super().restart()
129
+
130
+ # PMaude uses Maude's random symbol, which is memoryless
131
+ # and deterministic for a fixed seed, so we need a new seed
132
+ if self.random:
133
+ self.random.copy().reduce()
134
+ maude.setRandomSeed(random.getrandbits(31))
135
+
136
+ def next_step(self):
137
+ """Perform a step of the simulation"""
138
+
139
+ # Application of any executable rule
140
+ # (assumming it is deterministic)
141
+ next_state, *_ = next(self.state.apply(None), (None,))
142
+
143
+ if next_state is not None:
144
+ self.state = next_state
145
+ self.step += 1
146
+
147
+
108
148
  def all_children(graph, state):
109
149
  """All children of a state in a graph"""
110
150
 
@@ -275,8 +315,8 @@ class PMaudeSimulator(BaseSimulator):
275
315
  self.state.rewrite()
276
316
 
277
317
  # Try to find Maude's random symbol for calculating random(1). If the
278
- # PMaude specification only reduces random(0), even after resetting
279
- # the random seed for a new simulation, it will take the same value
318
+ # PMaude specification only reduces random(0), even after resetting the
319
+ # random seed for a new simulation, it will take the same cached value
280
320
  self.random = self.module.findSymbol('random', (nat_kind,), nat_kind)
281
321
 
282
322
  if self.random:
@@ -469,12 +509,15 @@ def get_simulator(method, data):
469
509
  method = 'step' if data.strategy else 'uniform'
470
510
 
471
511
  # Check whether a strategy is provided for methods that require it
472
- if not data.strategy and method in ('step', 'strategy-fast', 'strategy'):
512
+ if not data.strategy and method in ('strategy-fast', 'strategy'):
473
513
  usermsgs.print_error(f'No strategy is provided for the {method} assignment method.')
474
514
  return None
475
515
 
476
516
  if method == 'step':
477
- return StrategyStepSimulator(data.term, data.strategy)
517
+ if data.strategy:
518
+ return StrategyStepSimulator(data.term, data.strategy)
519
+ else:
520
+ return RuleStepSimulator(data.term)
478
521
 
479
522
  if method == 'strategy-fast':
480
523
  return StrategyPathSimulator(data.module, data.term, data.strategy)