_core.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. # -*- test-case-name: automat._test.test_core -*-
  2. """
  3. A core state-machine abstraction.
  4. Perhaps something that could be replaced with or integrated into machinist.
  5. """
  6. from __future__ import annotations
  7. import sys
  8. from itertools import chain
  9. from typing import Callable, Generic, Optional, Sequence, TypeVar, Hashable
  10. if sys.version_info >= (3, 10):
  11. from typing import TypeAlias
  12. else:
  13. from typing_extensions import TypeAlias
  14. _NO_STATE = "<no state>"
  15. State = TypeVar("State", bound=Hashable)
  16. Input = TypeVar("Input", bound=Hashable)
  17. Output = TypeVar("Output", bound=Hashable)
  18. class NoTransition(Exception, Generic[State, Input]):
  19. """
  20. A finite state machine in C{state} has no transition for C{symbol}.
  21. @ivar state: See C{state} init parameter.
  22. @ivar symbol: See C{symbol} init parameter.
  23. """
  24. def __init__(self, state: State, symbol: Input):
  25. """
  26. Construct a L{NoTransition}.
  27. @param state: the finite state machine's state at the time of the
  28. illegal transition.
  29. @param symbol: the input symbol for which no transition exists.
  30. """
  31. self.state = state
  32. self.symbol = symbol
  33. super(Exception, self).__init__(
  34. "no transition for {} in {}".format(symbol, state)
  35. )
  36. class Automaton(Generic[State, Input, Output]):
  37. """
  38. A declaration of a finite state machine.
  39. Note that this is not the machine itself; it is immutable.
  40. """
  41. def __init__(self, initial: State | None = None) -> None:
  42. """
  43. Initialize the set of transitions and the initial state.
  44. """
  45. if initial is None:
  46. initial = _NO_STATE # type:ignore[assignment]
  47. assert initial is not None
  48. self._initialState: State = initial
  49. self._transitions: set[tuple[State, Input, State, Sequence[Output]]] = set()
  50. self._unhandledTransition: Optional[tuple[State, Sequence[Output]]] = None
  51. @property
  52. def initialState(self) -> State:
  53. """
  54. Return this automaton's initial state.
  55. """
  56. return self._initialState
  57. @initialState.setter
  58. def initialState(self, state: State) -> None:
  59. """
  60. Set this automaton's initial state. Raises a ValueError if
  61. this automaton already has an initial state.
  62. """
  63. if self._initialState is not _NO_STATE:
  64. raise ValueError(
  65. "initial state already set to {}".format(self._initialState)
  66. )
  67. self._initialState = state
  68. def addTransition(
  69. self,
  70. inState: State,
  71. inputSymbol: Input,
  72. outState: State,
  73. outputSymbols: tuple[Output, ...],
  74. ):
  75. """
  76. Add the given transition to the outputSymbol. Raise ValueError if
  77. there is already a transition with the same inState and inputSymbol.
  78. """
  79. # keeping self._transitions in a flat list makes addTransition
  80. # O(n^2), but state machines don't tend to have hundreds of
  81. # transitions.
  82. for anInState, anInputSymbol, anOutState, _ in self._transitions:
  83. if anInState == inState and anInputSymbol == inputSymbol:
  84. raise ValueError(
  85. "already have transition from {} to {} via {}".format(
  86. inState, anOutState, inputSymbol
  87. )
  88. )
  89. self._transitions.add((inState, inputSymbol, outState, tuple(outputSymbols)))
  90. def unhandledTransition(
  91. self, outState: State, outputSymbols: Sequence[Output]
  92. ) -> None:
  93. """
  94. All unhandled transitions will be handled by transitioning to the given
  95. error state and error-handling output symbols.
  96. """
  97. self._unhandledTransition = (outState, tuple(outputSymbols))
  98. def allTransitions(self) -> frozenset[tuple[State, Input, State, Sequence[Output]]]:
  99. """
  100. All transitions.
  101. """
  102. return frozenset(self._transitions)
  103. def inputAlphabet(self) -> set[Input]:
  104. """
  105. The full set of symbols acceptable to this automaton.
  106. """
  107. return {
  108. inputSymbol
  109. for (inState, inputSymbol, outState, outputSymbol) in self._transitions
  110. }
  111. def outputAlphabet(self) -> set[Output]:
  112. """
  113. The full set of symbols which can be produced by this automaton.
  114. """
  115. return set(
  116. chain.from_iterable(
  117. outputSymbols
  118. for (inState, inputSymbol, outState, outputSymbols) in self._transitions
  119. )
  120. )
  121. def states(self) -> frozenset[State]:
  122. """
  123. All valid states; "Q" in the mathematical description of a state
  124. machine.
  125. """
  126. return frozenset(
  127. chain.from_iterable(
  128. (inState, outState)
  129. for (inState, inputSymbol, outState, outputSymbol) in self._transitions
  130. )
  131. )
  132. def outputForInput(
  133. self, inState: State, inputSymbol: Input
  134. ) -> tuple[State, Sequence[Output]]:
  135. """
  136. A 2-tuple of (outState, outputSymbols) for inputSymbol.
  137. """
  138. for anInState, anInputSymbol, outState, outputSymbols in self._transitions:
  139. if (inState, inputSymbol) == (anInState, anInputSymbol):
  140. return (outState, list(outputSymbols))
  141. if self._unhandledTransition is None:
  142. raise NoTransition(state=inState, symbol=inputSymbol)
  143. return self._unhandledTransition
  144. OutputTracer = Callable[[Output], None]
  145. Tracer: TypeAlias = "Callable[[State, Input, State], OutputTracer[Output] | None]"
  146. class Transitioner(Generic[State, Input, Output]):
  147. """
  148. The combination of a current state and an L{Automaton}.
  149. """
  150. def __init__(self, automaton: Automaton[State, Input, Output], initialState: State):
  151. self._automaton: Automaton[State, Input, Output] = automaton
  152. self._state: State = initialState
  153. self._tracer: Tracer[State, Input, Output] | None = None
  154. def setTrace(self, tracer: Tracer[State, Input, Output] | None) -> None:
  155. self._tracer = tracer
  156. def transition(
  157. self, inputSymbol: Input
  158. ) -> tuple[Sequence[Output], OutputTracer[Output] | None]:
  159. """
  160. Transition between states, returning any outputs.
  161. """
  162. outState, outputSymbols = self._automaton.outputForInput(
  163. self._state, inputSymbol
  164. )
  165. outTracer = None
  166. if self._tracer:
  167. outTracer = self._tracer(self._state, inputSymbol, outState)
  168. self._state = outState
  169. return (outputSymbols, outTracer)