umaudemc 0.12.2__py3-none-any.whl → 0.13.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/pyslang.py CHANGED
@@ -19,7 +19,7 @@ class Instruction:
19
19
  POP = 0 # no arguments
20
20
  JUMP = 1 # sequence of addresses
21
21
  STRAT = 2 # strategy (at the object level, for potential optimizations only)
22
- RLAPP = 3 # (label, intial substitution, whether it is applied on top)
22
+ RLAPP = 3 # (label, initial substitution, whether it is applied on top)
23
23
  TEST = 4 # (match type, pattern, condition)
24
24
  CHOICE = 5 # sequence of pairs of term (weight) and address
25
25
  CALL = 6 # (call term, index of a named strategy within the program, whether the call is tail)
@@ -34,10 +34,11 @@ class Instruction:
34
34
  WMATCHREW = 15 # like MATCHREW + (weight)
35
35
  ONE = 16 # no arguments
36
36
  CHECKPOINT = 17 # no arguments
37
+ KLEENE = 18 # (address of where the iteration begins, whether it enters or leaves)
37
38
 
38
39
  NAMES = ['POP', 'JUMP', 'STRAT', 'RLAPP', 'TEST', 'CHOICE', 'CALL', 'SUBSEARCH',
39
40
  'NOFAIL', 'MATCHREW', 'NEXTSUBTERM', 'RWCSTART', 'RWCNEXT', 'NOTIFY',
40
- 'SAMPLE', 'WMATCHREW', 'ONE', 'CHECKPOINT']
41
+ 'SAMPLE', 'WMATCHREW', 'ONE', 'CHECKPOINT', 'KLEENE']
41
42
 
42
43
  def __init__(self, itype, extra=None):
43
44
  self.type = itype
@@ -209,7 +210,7 @@ class ListWalker:
209
210
  class StratCompiler:
210
211
  """Compiler of strategy expressions"""
211
212
 
212
- def __init__(self, m, ml, use_notify=False, ignore_one=False):
213
+ def __init__(self, m, ml, use_notify=False, use_kleene=False, ignore_one=False):
213
214
  # Metalevel module
214
215
  self.ml = ml
215
216
  # Object-level module where the strategy lives
@@ -219,6 +220,8 @@ class StratCompiler:
219
220
  self.use_notify = use_notify
220
221
  # Whether the one combinator is ignored or not
221
222
  self.use_one = not ignore_one
223
+ # Whether to delimit iterations with the Kleene instruction
224
+ self.use_kleene = use_kleene
222
225
 
223
226
  # Table of strategy combinators
224
227
  self.op_table = {}
@@ -453,22 +456,34 @@ class StratCompiler:
453
456
  def ne_iter(self, s, p, tail):
454
457
  initial_pc = p.pc
455
458
 
459
+ if self.use_kleene:
460
+ p.append(Instruction.KLEENE, (initial_pc, True))
461
+
456
462
  # The non-empty iteration executes the body and then jumps
457
463
  # to the next instruction or to the body again
458
464
  self.generate(next(s.arguments()), p, False)
459
465
 
460
466
  p.append(Instruction.JUMP, (p.pc + 1, initial_pc))
461
467
 
468
+ if self.use_kleene:
469
+ p.append(Instruction.KLEENE, (initial_pc, False))
470
+
462
471
  def iter(self, s, p, tail):
463
472
  # The empty iteration does the same with an additional initial jump
464
473
  initial_jump = p.append(Instruction.JUMP)
465
474
  initial_pc = p.pc
466
475
 
476
+ if self.use_kleene:
477
+ p.append(Instruction.KLEENE, (initial_pc, True))
478
+
467
479
  self.generate(next(s.arguments()), p, False)
468
480
 
469
481
  initial_jump.extra = (p.pc + 1, initial_pc)
470
482
  p.append(Instruction.JUMP, (p.pc + 1, initial_pc))
471
483
 
484
+ if self.use_kleene:
485
+ p.append(Instruction.KLEENE, (initial_pc, False))
486
+
472
487
  def cond(self, s, p, tail):
473
488
  condition, true_branch, false_branch = s.arguments()
474
489
 
@@ -907,7 +922,7 @@ class StratCompiler:
907
922
  visited.add(next_state)
908
923
  dfs_stack.append(next_state)
909
924
 
910
- # The reachable field has been calculated assumming that calls do not apply rewrites,
925
+ # The reachable field has been calculated assuming that calls do not apply rewrites,
911
926
  # but we can consider they do if every definition for it applies a rewrite
912
927
 
913
928
  # has_rewrite_strats holds for each strategy the definitions that do not apply a rewrite
@@ -979,7 +994,7 @@ class StratCompiler:
979
994
  jump_over_notify = [] # Jumps that should be added to skip notifies
980
995
  # Blocks that carry a notify to the block in the key
981
996
  notifiers = {}
982
- # Blocks that jump to to the block in the key without address
997
+ # Blocks that jump to the block in the key without address
983
998
  not_notifiers = {}
984
999
 
985
1000
  # Backward jumps are always non-notifying
@@ -1198,6 +1213,13 @@ class StackNode:
1198
1213
 
1199
1214
  self.seen[(pc, term, self.venv)] = value
1200
1215
 
1216
+ def __eq__(self, other):
1217
+ """Deep comparison of stack nodes"""
1218
+ return self.pc == other.pc and self.venv == other.venv and self.parent == other.parent
1219
+
1220
+ def __hash__(self):
1221
+ return hash((self.pc, self.venv, self.parent))
1222
+
1201
1223
  def __repr__(self):
1202
1224
  return f'StackNode({self.venv}, {self.pc})'
1203
1225
 
@@ -1215,6 +1237,12 @@ class RwcNode(StackNode):
1215
1237
  # Matching context
1216
1238
  self.context = context
1217
1239
 
1240
+ def __eq__(self, other):
1241
+ return self is other
1242
+
1243
+ def __hash__(self):
1244
+ return id(self)
1245
+
1218
1246
  def __repr__(self):
1219
1247
  return f'RwcNode({self.venv}, index={self.index})'
1220
1248
 
@@ -1239,6 +1267,13 @@ class SubtermNode(StackNode):
1239
1267
  pending=self.pending[1:], done={**self.done, **{var: value}},
1240
1268
  seen=self.seen.fork((pc, value)))
1241
1269
 
1270
+ def __eq__(self, other):
1271
+ return type(other) is type(self) and self.context == other.context and \
1272
+ self.done == other.done and len(self.pending) == len(other.pending)
1273
+
1274
+ def __hash__(self):
1275
+ return hash((self.context, self.donde, len(self.pending), super()))
1276
+
1242
1277
  def __repr__(self):
1243
1278
  return f'SubtermNode({self.venv}, {self.context}, {self.done}, {self.pending})'
1244
1279
 
@@ -1353,12 +1388,17 @@ class StratRunner:
1353
1388
  Instruction.WMATCHREW: self.wmatchrew,
1354
1389
  Instruction.ONE: self.one,
1355
1390
  Instruction.CHECKPOINT: self.checkpoint,
1391
+ Instruction.KLEENE: self.kleene,
1356
1392
  }
1357
1393
 
1394
+ # Number of rewrites
1395
+ self.nr_rewrites = 0
1396
+
1358
1397
  def reset(self, term):
1359
1398
  """Reset the runner with a given initial term"""
1360
1399
 
1361
1400
  self.current_state = self.state_class(term, 0, self.stack_root)
1401
+ self.nr_rewrites = 0
1362
1402
  self.pending.clear()
1363
1403
 
1364
1404
  def dump_state(self):
@@ -1481,7 +1521,7 @@ class StratRunner:
1481
1521
  initial_subs = {var: stack.venv.instantiate(value) for var, value in initial_subs.items()}
1482
1522
 
1483
1523
  for value in initial_subs.values():
1484
- value.reduce()
1524
+ self.nr_rewrites += value.reduce()
1485
1525
 
1486
1526
  initial_subs = maude.Substitution(initial_subs)
1487
1527
 
@@ -1489,10 +1529,15 @@ class StratRunner:
1489
1529
  initial_subs,
1490
1530
  maxDepth=-1 if top else maude.UNBOUNDED)
1491
1531
 
1532
+ def reduced(self, t):
1533
+ """Return the given term reduced and count the rewrites"""
1534
+ self.nr_rewrites += t.reduce() + 1
1535
+ return t
1536
+
1492
1537
  def rlapp(self, args, stack):
1493
1538
  """Apply a rule"""
1494
1539
 
1495
- self.pending += [self.current_state.copy(term=(t.reduce(), t)[1])
1540
+ self.pending += [self.current_state.copy(term=self.reduced(t))
1496
1541
  for t, *_ in self.get_rewrites(args, stack)]
1497
1542
 
1498
1543
  self.next_pending()
@@ -1550,8 +1595,7 @@ class StratRunner:
1550
1595
  except ValueError:
1551
1596
  self.next_pending()
1552
1597
 
1553
- @staticmethod
1554
- def compute_weights(weights, venv):
1598
+ def compute_weights(self, weights, venv):
1555
1599
  """Obtain the weights of a choice"""
1556
1600
 
1557
1601
  # Instantiate and reduce the weights
@@ -1559,7 +1603,7 @@ class StratRunner:
1559
1603
  weights = [venv.instantiate(w) for w in weights]
1560
1604
 
1561
1605
  for w in weights:
1562
- w.reduce()
1606
+ self.nr_rewrites += w.reduce()
1563
1607
 
1564
1608
  # Convert the weights to Python's float
1565
1609
  # (unreduced weights will be silently converted to zero)
@@ -1578,7 +1622,7 @@ class StratRunner:
1578
1622
  # Instantiate the call term with the variable context
1579
1623
  if stack.venv:
1580
1624
  call = stack.venv.instantiate(call)
1581
- call.reduce()
1625
+ self.nr_rewrites += call.reduce()
1582
1626
 
1583
1627
  # Match the call term with every definition
1584
1628
  pending = []
@@ -1661,7 +1705,7 @@ class StratRunner:
1661
1705
 
1662
1706
  # Instantiated weight
1663
1707
  this_weight = merged_subs.instantiate(weight)
1664
- this_weight.reduce()
1708
+ self.nr_rewrites += this_weight.reduce()
1665
1709
 
1666
1710
  match_data.append((merged_subs, context))
1667
1711
  weights.append(float(this_weight))
@@ -1683,7 +1727,7 @@ class StratRunner:
1683
1727
 
1684
1728
  # Start evaluating the first subterm
1685
1729
  self.current_state = self.current_state.copy(term=merged_subs.instantiate(variables[0]),
1686
- stack=new_stack)
1730
+ stack=new_stack)
1687
1731
 
1688
1732
  @staticmethod
1689
1733
  def process_match(venv, match, ctx, pattern, variables):
@@ -1739,7 +1783,7 @@ class StratRunner:
1739
1783
  initial_subs = {var: stack.venv.instantiate(value) for var, value in initial_subs.items()}
1740
1784
 
1741
1785
  for value in initial_subs.values():
1742
- value.reduce()
1786
+ self.nr_rewrites += value.reduce()
1743
1787
 
1744
1788
  initial_subs_obj = maude.Substitution(initial_subs)
1745
1789
 
@@ -1758,7 +1802,7 @@ class StratRunner:
1758
1802
  new_stack = RwcNode(parent=stack, index=k, subs=subs, context=(ctx(hole), hole))
1759
1803
  # Start rewriting the left-hand side of the first condition fragment
1760
1804
  new_term = subs.instantiate(rwc_lhs)
1761
- new_term.reduce()
1805
+ self.nr_rewrites += new_term.reduce()
1762
1806
  self.pending.append(self.current_state.copy(term=new_term, stack=new_stack))
1763
1807
 
1764
1808
  self.next_pending()
@@ -1781,12 +1825,12 @@ class StratRunner:
1781
1825
  if is_final:
1782
1826
  context, hole = stack.context
1783
1827
  new_term = maude.Substitution({hole: subs.instantiate(rwc_lhs)}).instantiate(context)
1784
- new_term.reduce()
1828
+ self.nr_rewrites += new_term.reduce()
1785
1829
  self.pending.append(self.current_state.copy(term=new_term, stack=stack.parent))
1786
1830
  else:
1787
1831
  new_stack = RwcNode(parent=stack.parent, index=stack.index, subs=subs, context=stack.context)
1788
1832
  new_term = subs.instantiate(rwc_lhs)
1789
- new_term.reduce()
1833
+ self.nr_rewrites += new_term.reduce()
1790
1834
  self.pending.append(self.current_state.copy(term=new_term, stack=new_stack))
1791
1835
 
1792
1836
  self.next_pending()
@@ -1801,7 +1845,7 @@ class StratRunner:
1801
1845
 
1802
1846
  # Reduce them
1803
1847
  for arg in dargs:
1804
- arg.reduce()
1848
+ self.nr_rewrites += arg.reduce()
1805
1849
 
1806
1850
  dargs = [float(arg) for arg in dargs]
1807
1851
 
@@ -1825,6 +1869,12 @@ class StratRunner:
1825
1869
  # Do nothing (subclasses will)
1826
1870
  self.current_state.pc += 1
1827
1871
 
1872
+ def kleene(self, args, stack):
1873
+ """Keep track of iterations"""
1874
+
1875
+ # Do nothing (subclasses will)
1876
+ self.current_state.pc += 1
1877
+
1828
1878
 
1829
1879
  def rebuild_term(term, stack, args):
1830
1880
  """Rebuild the term within nested matchrews"""
@@ -1848,7 +1898,7 @@ def rebuild_term(term, stack, args):
1848
1898
  subs = maude.Substitution({
1849
1899
  # ...the subterms that have already been rewritten, and...
1850
1900
  **mrew_stack.done,
1851
- # ..the current and the pending subterms.
1901
+ # ...the current and the pending subterms.
1852
1902
  **dict(zip(pending_vars, [new_term] + mrew_stack.pending))
1853
1903
  })
1854
1904
  # Rebuild the subterm
@@ -1882,6 +1932,35 @@ class GraphExecutionState(StratRunner.State):
1882
1932
  self.extra if extra is None else extra,
1883
1933
  )
1884
1934
 
1935
+ def reset_extra(self):
1936
+ """Reset the extra field"""
1937
+ pass # subclasses will
1938
+
1939
+
1940
+ class GraphState:
1941
+ """State of the rewrite graph"""
1942
+
1943
+ def __init__(self, term):
1944
+ self.term = term
1945
+ # Set of graph states that follow by a rewrite
1946
+ self.children = set()
1947
+ # Temporary information about the last rewrite
1948
+ self.last_rewrite = None
1949
+ self.actions = {}
1950
+ self.solution = False
1951
+ self.valid = True
1952
+ # Each graph state has its own list of pending execution states
1953
+ self.pending = []
1954
+ # Subsearches for which a solution is reachable from here
1955
+ self.solve_subsearch = set()
1956
+
1957
+ @property
1958
+ def has_children(self):
1959
+ return self.children
1960
+
1961
+ def __repr__(self):
1962
+ return f'GraphState({self.term}, {self.children}, {self.solution})'
1963
+
1885
1964
 
1886
1965
  class BadProbStrategy(Exception):
1887
1966
  """Bad probabilitistic strategy that cannot engender MDPs or DTMCs"""
@@ -1906,40 +1985,14 @@ class SubsearchNode(StackNode):
1906
1985
  return f'SubsearchNode(id={self.subsearch_id}, dfs_base={self.dfs_base})'
1907
1986
 
1908
1987
 
1909
- class MarkovRunner(StratRunner):
1910
- """Runner that extracts a DTMC or MDP"""
1911
-
1912
- class GraphState:
1913
- """State of the graph
1914
-
1915
- In addition to the actual states of the graph, there may be temporary fake
1916
- states used to gather the evolutions of the branches of a choice. They are
1917
- characterized by a None value in their term attribute.
1918
- """
1919
-
1920
- def __init__(self, term):
1921
- self.term = term
1922
- # Set of graph states that follow by a rewrite
1923
- self.children = set()
1924
- # Set of choice alternatives (each an association of
1925
- # weights or probabilities to graph states)
1926
- self.child_choices = set()
1927
- # Temporary information about the last rewrite
1928
- self.last_rewrite = None
1929
- self.actions = {}
1930
- self.solution = False
1931
- self.valid = True
1932
- # Each graph state has its own list of pending execution states
1933
- self.pending = []
1934
- # Subsearches for which a solution is reachable from here
1935
- self.solve_subsearch = set()
1936
-
1937
- def __repr__(self):
1938
- return f'GraphState({self.term}, {self.children}, {self.child_choices}, {self.solution})'
1988
+ class GraphRunner(StratRunner):
1989
+ """Runner that extracts a graph"""
1939
1990
 
1940
- def __init__(self, program, term):
1941
- super().__init__(program, term, state_class=GraphExecutionState, seen_class=dict)
1942
- self.root_node = self.GraphState(term)
1991
+ def __init__(self, program, term, state_class=GraphExecutionState, graph_state_class=GraphState):
1992
+ super().__init__(program, term, state_class=state_class, seen_class=dict)
1993
+ # Class of the states of the graph
1994
+ self.graph_state_class = graph_state_class
1995
+ self.root_node = graph_state_class(term)
1943
1996
  self.dfs_stack = [self.root_node]
1944
1997
 
1945
1998
  # A dictionary from execution states to graph states: the latter
@@ -1951,59 +2004,13 @@ class MarkovRunner(StratRunner):
1951
2004
  # different sucessor can be reached)
1952
2005
  self.solution_states = {}
1953
2006
 
1954
- def resolve_choice(self, choice, actions):
1955
- """Resolve a choice with weights into probabilities"""
1956
-
1957
- # While choice is a sequence of (weight, state), new_choice is
1958
- # a dictionary from state to probability
1959
- new_choice = {}
1960
-
1961
- for w, s in choice:
1962
- if not s.valid:
1963
- continue
1964
-
1965
- # If s is not a fake state
1966
- if s.term is not None:
1967
- old_w = new_choice.get(s, 0.0)
1968
- new_choice[s] = old_w + w
1969
-
1970
- else:
1971
- # Nondeteterministic and choice alternatives
1972
- nd_opts, ch_opts = len(s.children), len(s.child_choices)
1973
-
1974
- if nd_opts + ch_opts > 1:
1975
- raise BadProbStrategy
1976
-
1977
- # Copy the action information
1978
- for target, info in s.actions.items():
1979
- old_info = actions.get(target)
1980
-
1981
- if old_info:
1982
- old_info.extend(info)
1983
- else:
1984
- actions[target] = info
1985
-
1986
- if nd_opts:
1987
- s, = s.children
1988
- old_w = new_choice.get(s, 0.0)
1989
- new_choice[s] = old_w + w
1990
-
1991
- if ch_opts:
1992
- child_choice, = s.child_choices
1993
- total_cw = sum(child_choice.values())
1994
- for cs, cw in child_choice.items():
1995
- old_w = new_choice.get(cs, 0.0)
1996
- new_choice[cs] = old_w + cw * w / total_cw
1997
-
1998
- return new_choice
1999
-
2000
2007
  def get_solution_state(self, term):
2001
2008
  """Get the solution state for the given term"""
2002
2009
 
2003
2010
  solution_state = self.solution_states.get(term)
2004
2011
 
2005
2012
  if not solution_state:
2006
- solution_state = self.GraphState(term)
2013
+ solution_state = self.graph_state_class(term)
2007
2014
  self.solution_states[term] = solution_state
2008
2015
 
2009
2016
  return solution_state
@@ -2028,7 +2035,7 @@ class MarkovRunner(StratRunner):
2028
2035
  return True
2029
2036
 
2030
2037
  # This execution state is linked to a fake choice-generated graph state
2031
- # that should be pused to the DFS stack
2038
+ # that should be pushed to the DFS stack
2032
2039
  elif push_state:
2033
2040
  self.dfs_stack.append(push_state)
2034
2041
  self.pending = push_state.pending
@@ -2038,37 +2045,24 @@ class MarkovRunner(StratRunner):
2038
2045
  return True
2039
2046
 
2040
2047
  # Check whether the graph state is valid
2041
- graph_state.valid = graph_state.solution or graph_state.children or graph_state.child_choices
2048
+ graph_state.valid = graph_state.solution or graph_state.has_children
2042
2049
 
2043
2050
  # Link a solution state if there are children
2044
2051
  # (these states are needed to explicit the self-loop for the stuttering
2045
2052
  # extension of finite traces without introducing spurious executions)
2046
- if graph_state.solution and (graph_state.children or graph_state.child_choices) and \
2053
+ if graph_state.solution and graph_state.has_children and \
2047
2054
  graph_state not in graph_state.children:
2048
2055
  solution_state = self.get_solution_state(graph_state.term)
2049
2056
  graph_state.children.add(solution_state)
2050
2057
  graph_state.actions[solution_state] = None
2051
2058
 
2052
- # Adjust the probilities of the choice operators
2053
- new_choices = []
2054
-
2055
- for choice in graph_state.child_choices:
2056
- resolved_choice = self.resolve_choice(choice, graph_state.actions)
2057
-
2058
- if len(resolved_choice) == 1:
2059
- child, = resolved_choice.keys()
2060
- graph_state.children.add(child)
2061
-
2062
- elif resolved_choice:
2063
- new_choices.append(resolved_choice)
2064
-
2065
- graph_state.child_choices = tuple(new_choices)
2059
+ self._on_state_exhausted(graph_state)
2066
2060
 
2067
2061
  self.dfs_stack.pop()
2068
2062
 
2069
2063
  if self.dfs_stack:
2070
2064
  dfs_top = self.dfs_stack[-1]
2071
- self.pending = dfs_top.pending
2065
+ self.pending = self.dfs_stack[-1].pending
2072
2066
 
2073
2067
  # Add graph_state as a child if it is valid
2074
2068
  if graph_state.valid and graph_state.term is not None:
@@ -2080,6 +2074,9 @@ class MarkovRunner(StratRunner):
2080
2074
  self.current_state = None
2081
2075
  return False
2082
2076
 
2077
+ def _on_state_exhausted(self, graph_state):
2078
+ pass # nothing to do here
2079
+
2083
2080
  def pop(self, _, stack):
2084
2081
  """Return from a strategy call or similar construct"""
2085
2082
 
@@ -2095,9 +2092,9 @@ class MarkovRunner(StratRunner):
2095
2092
  else:
2096
2093
  graph_state = self.dfs_stack[-1]
2097
2094
 
2098
- # Fake states are not marked as solutions but added a
2099
- # solution state as a child (this is also done later to
2100
- # marked states unless they do not have successors)
2095
+ # Fake states (used in the subclass MarkovRunner) are not marked
2096
+ # as solutions but added a solution state as a child (this is also
2097
+ # done later to marked states unless they do not have successors)
2101
2098
  if graph_state.term is None:
2102
2099
  graph_state.children.add(self.get_solution_state(self.current_state.term))
2103
2100
  else:
@@ -2105,6 +2102,202 @@ class MarkovRunner(StratRunner):
2105
2102
 
2106
2103
  self.next_pending()
2107
2104
 
2105
+ def notify(self, args, stack):
2106
+ """Record a transition in the graph"""
2107
+
2108
+ state = self.current_state
2109
+ state.pc += 1
2110
+
2111
+ # Check whether this state has already been visited
2112
+ successor = stack.already_seen_table(state.pc, state.term)
2113
+
2114
+ # This is a new state, so add it to the graph
2115
+ if successor is None:
2116
+ new_term = rebuild_term(state.term, stack, args)
2117
+
2118
+ new_state = self.graph_state_class(new_term)
2119
+ stack.add_to_seen_table(state.pc, state.term, new_state)
2120
+ self.dfs_stack.append(new_state)
2121
+ self.pending = new_state.pending
2122
+
2123
+ # The extra information for the last rewrite is recorded
2124
+ self.dfs_stack[-2].last_rewrite = state.extra
2125
+ state.reset_extra()
2126
+
2127
+ else:
2128
+ if successor.valid:
2129
+ dfs_top = self.dfs_stack[-1]
2130
+ dfs_top.children.add(successor)
2131
+ dfs_top.actions.setdefault(successor, []).append(state.extra)
2132
+
2133
+ state.reset_extra()
2134
+
2135
+ # Handle the deactivation of the negative branch of
2136
+ # a subsearch when this state leaded to a solution
2137
+ to_be_solved = len(successor.solve_subsearch)
2138
+
2139
+ while stack and to_be_solved:
2140
+ subsearch_id = getattr(stack, 'subsearch_id', None)
2141
+ if subsearch_id in successor.solve_subsearch:
2142
+ stack.pc = None
2143
+ to_be_solved -= 1
2144
+ stack = stack.parent
2145
+
2146
+ self.next_pending()
2147
+
2148
+ def checkpoint(self, args, stack):
2149
+ """Check whether this state has been visited and interrupt the execution"""
2150
+
2151
+ if stack.already_seen_table(self.current_state.pc, self.dfs_stack[-1]):
2152
+ self.next_pending()
2153
+
2154
+ else:
2155
+ stack.add_to_seen_table(self.current_state.pc, self.dfs_stack[-1], True)
2156
+ self.current_state.pc += 1
2157
+
2158
+ def subsearch(self, args, stack):
2159
+ """Start a subsearch by pushing a stack node with continuation"""
2160
+
2161
+ # The same as StratRunner.subsearch, but forking the seen set. Moreover,
2162
+ # the size of the DFS stack is recorded to allow finding out which states
2163
+ # lead to solutions within the search (identified by the current PC)
2164
+ subsearch_stack = SubsearchNode(parent=stack, pc=args,
2165
+ seen=stack.seen.fork(self.current_state.pc),
2166
+ dfs_base=len(self.dfs_stack),
2167
+ subsearch_id=self.current_state.pc)
2168
+ self.current_state = self.current_state.copy(stack=subsearch_stack)
2169
+
2170
+ # Exactly the same as StratRunner.subsearch
2171
+ self.pending.append(self.current_state.copy(pc=args, stack=subsearch_stack, conditional=True))
2172
+
2173
+ def nofail(self, args, stack):
2174
+ """Pop the stack and discard the continuation on failure"""
2175
+
2176
+ # Annotate the graph states with the current subsearch,
2177
+ # for which they provide a solution
2178
+ for node in self.dfs_stack[stack.dfs_base:]:
2179
+ node.solve_subsearch.add(stack.subsearch_id)
2180
+
2181
+ super().nofail(args, stack)
2182
+
2183
+ def rlapp(self, args, stack):
2184
+ """Apply a rule"""
2185
+
2186
+ # Like the parent implementation, but the rule is recorded
2187
+ self.pending += [self.current_state.copy(term=self.reduced(t), extra=rl)
2188
+ for t, _, _, rl in self.get_rewrites(args, stack)]
2189
+
2190
+ self.next_pending()
2191
+
2192
+ def run(self):
2193
+ """Run the strategy to generate the graph"""
2194
+
2195
+ self.solution = None
2196
+ self.pending = self.root_node.pending
2197
+
2198
+ while self.current_state:
2199
+ state = self.current_state
2200
+
2201
+ # The instruction to be executed
2202
+ inst = self.code[state.pc]
2203
+
2204
+ self.handlers[inst.type](inst.extra, state.stack)
2205
+
2206
+ return self.root_node, self.nr_rewrites
2207
+
2208
+
2209
+ class MarkovRunner(GraphRunner):
2210
+ """Runner that extracts a DTMC or MDP"""
2211
+
2212
+ class GraphState(GraphState):
2213
+ """State of the graph
2214
+
2215
+ In addition to the actual states of the graph, there may be temporary fake
2216
+ states used to gather the evolutions of the branches of a choice. They are
2217
+ characterized by a None value in their term attribute.
2218
+ """
2219
+
2220
+ def __init__(self, term):
2221
+ super().__init__(term)
2222
+
2223
+ # Set of choice alternatives (each an association of
2224
+ # weights or probabilities to graph states)
2225
+ self.child_choices = set()
2226
+
2227
+ @property
2228
+ def has_children(self):
2229
+ return self.children or self.child_choices
2230
+
2231
+ def __repr__(self):
2232
+ return f'GraphState({self.term}, {self.children}, {self.child_choices}, {self.solution})'
2233
+
2234
+ def __init__(self, program, term):
2235
+ super().__init__(program, term, graph_state_class=self.GraphState)
2236
+
2237
+ def resolve_choice(self, choice, actions):
2238
+ """Resolve a choice with weights into probabilities"""
2239
+
2240
+ # While choice is a sequence of (weight, state), new_choice is
2241
+ # a dictionary from state to probability
2242
+ new_choice = {}
2243
+
2244
+ for w, s in choice:
2245
+ if not s.valid:
2246
+ continue
2247
+
2248
+ # If s is not a fake state
2249
+ if s.term is not None:
2250
+ old_w = new_choice.get(s, 0.0)
2251
+ new_choice[s] = old_w + w
2252
+
2253
+ else:
2254
+ # Nondeterministic and choice alternatives
2255
+ nd_opts, ch_opts = len(s.children), len(s.child_choices)
2256
+
2257
+ if nd_opts + ch_opts > 1:
2258
+ raise BadProbStrategy
2259
+
2260
+ # Copy the action information
2261
+ for target, info in s.actions.items():
2262
+ old_info = actions.get(target)
2263
+
2264
+ if old_info:
2265
+ old_info.extend(info)
2266
+ else:
2267
+ actions[target] = info
2268
+
2269
+ if nd_opts:
2270
+ s, = s.children
2271
+ old_w = new_choice.get(s, 0.0)
2272
+ new_choice[s] = old_w + w
2273
+
2274
+ if ch_opts:
2275
+ child_choice, = s.child_choices
2276
+ total_cw = sum(child_choice.values())
2277
+ for cs, cw in child_choice.items():
2278
+ old_w = new_choice.get(cs, 0.0)
2279
+ new_choice[cs] = old_w + cw * w / total_cw
2280
+
2281
+ return new_choice
2282
+
2283
+ def _on_state_exhausted(self, graph_state):
2284
+ """When a graph state is exhausted"""
2285
+
2286
+ # Adjust the probabilities of the choice operators
2287
+ new_choices = []
2288
+
2289
+ for choice in graph_state.child_choices:
2290
+ resolved_choice = self.resolve_choice(choice, graph_state.actions)
2291
+
2292
+ if len(resolved_choice) == 1:
2293
+ child, = resolved_choice.keys()
2294
+ graph_state.children.add(child)
2295
+
2296
+ elif resolved_choice:
2297
+ new_choices.append(resolved_choice)
2298
+
2299
+ graph_state.child_choices = tuple(new_choices)
2300
+
2108
2301
  def choice(self, args, stack):
2109
2302
  """A choice node"""
2110
2303
 
@@ -2161,7 +2354,7 @@ class MarkovRunner(StratRunner):
2161
2354
 
2162
2355
  # Instantiated weight
2163
2356
  this_weight = merged_subs.instantiate(weight)
2164
- this_weight.reduce()
2357
+ self.nr_rewrites += this_weight.reduce()
2165
2358
  this_weight = float(this_weight)
2166
2359
 
2167
2360
  if this_weight > 0.0:
@@ -2242,65 +2435,6 @@ class MarkovRunner(StratRunner):
2242
2435
 
2243
2436
  self.next_pending()
2244
2437
 
2245
- def checkpoint(self, args, stack):
2246
- """Check whether this state has been visited and interrupt the execution"""
2247
-
2248
- if stack.already_seen_table(self.current_state.pc, self.dfs_stack[-1]):
2249
- self.next_pending()
2250
-
2251
- else:
2252
- stack.add_to_seen_table(self.current_state.pc, self.dfs_stack[-1], True)
2253
- self.current_state.pc += 1
2254
-
2255
- def subsearch(self, args, stack):
2256
- """Start a subsearch by pushing a stack node with continuation"""
2257
-
2258
- # The same as StratRunner.subsearch, but forking the seen set. Moreover,
2259
- # the size of the DFS stack is recorded to allow finding out which states
2260
- # lead to solutions within the search (identified by the current PC)
2261
- subsearch_stack = SubsearchNode(parent=stack, pc=args,
2262
- seen=stack.seen.fork(self.current_state.pc),
2263
- dfs_base=len(self.dfs_stack),
2264
- subsearch_id=self.current_state.pc)
2265
- self.current_state = self.current_state.copy(stack=subsearch_stack)
2266
-
2267
- # Exactly the same as StratRunner.subsearch
2268
- self.pending.append(self.current_state.copy(pc=args, stack=subsearch_stack, conditional=True))
2269
-
2270
- def nofail(self, args, stack):
2271
- """Pop the stack and discard the continuation on failure"""
2272
-
2273
- # Annotate the graph states with the current subsearch,
2274
- # for which they provides a solution
2275
- for node in self.dfs_stack[stack.dfs_base:]:
2276
- node.solve_subsearch.add(stack.subsearch_id)
2277
-
2278
- super().nofail(args, stack)
2279
-
2280
- def rlapp(self, args, stack):
2281
- """Apply a rule"""
2282
-
2283
- self.pending += [self.current_state.copy(term=(t.reduce(), t)[1], extra=rl)
2284
- for t, _, _, rl in self.get_rewrites(args, stack)]
2285
-
2286
- self.next_pending()
2287
-
2288
- def run(self):
2289
- """Run the strategy to generate the graph"""
2290
-
2291
- self.solution = None
2292
- self.pending = self.root_node.pending
2293
-
2294
- while self.current_state:
2295
- state = self.current_state
2296
-
2297
- # The instruction to be executed
2298
- inst = self.code[state.pc]
2299
-
2300
- self.handlers[inst.type](inst.extra, state.stack)
2301
-
2302
- return self.root_node
2303
-
2304
2438
 
2305
2439
  class MetadataRunner(MarkovRunner):
2306
2440
  """Runner for the metadata assignment method with non-ground weights"""
@@ -2319,7 +2453,7 @@ class MetadataRunner(MarkovRunner):
2319
2453
 
2320
2454
  for t, sb, _, rl in self.get_rewrites(args, stack):
2321
2455
  # Term.apply does not normalize output terms
2322
- t.reduce()
2456
+ self.nr_rewrites += t.reduce() + 1
2323
2457
 
2324
2458
  # Evaluate the metadata weight
2325
2459
  weight = self.stmt_map.get(rl, 1.0)
@@ -2327,7 +2461,7 @@ class MetadataRunner(MarkovRunner):
2327
2461
  # If it is not a literal, we should reduce the term
2328
2462
  if not isinstance(weight, float):
2329
2463
  weight = sb.instantiate(weight)
2330
- weight.reduce()
2464
+ self.nr_rewrites += weight.reduce()
2331
2465
  weight = float(weight)
2332
2466
 
2333
2467
  self.pending.append(self.current_state.copy(term=t, extra=(rl, weight)))
@@ -2338,7 +2472,7 @@ class MetadataRunner(MarkovRunner):
2338
2472
  class RandomRunner(StratRunner):
2339
2473
  """Runner that resolves every choice locally at random without backtracking.
2340
2474
 
2341
- Instead of solutions of the strategy, the run method returns the succesive steps
2475
+ Instead of solutions of the strategy, the run method returns the successive steps
2342
2476
  of a single random rewriting path. Hence, conditionals do no work with this runner,
2343
2477
  since exploration is not exhaustive, and failed executions are not discarded."""
2344
2478
 
@@ -2355,7 +2489,7 @@ class RandomRunner(StratRunner):
2355
2489
  subsearches, trivial_subsearch = [], False
2356
2490
 
2357
2491
  for k, inst in enumerate(self.code.inst):
2358
- # Jumps with multiple desinations
2492
+ # Jumps with multiple destinations
2359
2493
  if inst.type == Instruction.JUMP and len(inst.extra) > 1:
2360
2494
  inst.extra = inst.extra[:1]
2361
2495
  usermsgs.print_warning(f'Unquantified nondeterminism detected in {current_name}. '
@@ -2396,9 +2530,9 @@ class RandomRunner(StratRunner):
2396
2530
  return False
2397
2531
 
2398
2532
  # Otherwise, a pending state is chosen at random and all other pending
2399
- # states are discarded. Indeed, pending states are not actually pending but
2533
+ # states are discarded. Indeed, pending states are not actually pending, but
2400
2534
  # they have just been generated by a nondeterministic instruction (rule
2401
- # application, call, matchrew...) before calling this method. Reimplemeting
2535
+ # application, call, matchrew...) before calling this method. Reimplementing
2402
2536
  # these instructions is avoided by this trick.
2403
2537
  self.current_state = random.choice(self.pending)
2404
2538
  self.pending.clear()