| """Classes representing state-machine concepts""" | |
| class NFA: | |
| """A non deterministic finite automata | |
| A non deterministic automata is a form of a finite state | |
| machine. An NFA's rules are less restrictive than a DFA. | |
| The NFA rules are: | |
| * A transition can be non-deterministic and can result in | |
| nothing, one, or two or more states. | |
| * An epsilon transition consuming empty input is valid. | |
| Transitions consuming labeled symbols are also permitted. | |
| This class assumes that there is only one starting state and one | |
| accepting (ending) state. | |
| Attributes: | |
| name (str): The name of the rule the NFA is representing. | |
| start (NFAState): The starting state. | |
| end (NFAState): The ending state | |
| """ | |
| def __init__(self, start, end): | |
| self.name = start.rule_name | |
| self.start = start | |
| self.end = end | |
| def __repr__(self): | |
| return "NFA(start={}, end={})".format(self.start, self.end) | |
| def dump(self, writer=print): | |
| """Dump a graphical representation of the NFA""" | |
| todo = [self.start] | |
| for i, state in enumerate(todo): | |
| writer(" State", i, state is self.end and "(final)" or "") | |
| for arc in state.arcs: | |
| label = arc.label | |
| next = arc.target | |
| if next in todo: | |
| j = todo.index(next) | |
| else: | |
| j = len(todo) | |
| todo.append(next) | |
| if label is None: | |
| writer(" -> %d" % j) | |
| else: | |
| writer(" %s -> %d" % (label, j)) | |
| def dump_graph(self, writer): | |
| """Dump a DOT representation of the NFA""" | |
| writer('digraph %s_nfa {\n' % self.name) | |
| todo = [self.start] | |
| for i, state in enumerate(todo): | |
| writer(' %d [label="State %d %s"];\n' % (i, i, state is self.end and "(final)" or "")) | |
| for arc in state.arcs: | |
| label = arc.label | |
| next = arc.target | |
| if next in todo: | |
| j = todo.index(next) | |
| else: | |
| j = len(todo) | |
| todo.append(next) | |
| if label is None: | |
| writer(" %d -> %d [style=dotted label=ε];\n" % (i, j)) | |
| else: | |
| writer(" %d -> %d [label=%s];\n" % (i, j, label.replace("'", '"'))) | |
| writer('}\n') | |
| class NFAArc: | |
| """An arc representing a transition between two NFA states. | |
| NFA states can be connected via two ways: | |
| * A label transition: An input equal to the label must | |
| be consumed to perform the transition. | |
| * An epsilon transition: The transition can be taken without | |
| consuming any input symbol. | |
| Attributes: | |
| target (NFAState): The ending state of the transition arc. | |
| label (Optional[str]): The label that must be consumed to make | |
| the transition. An epsilon transition is represented | |
| using `None`. | |
| """ | |
| def __init__(self, target, label): | |
| self.target = target | |
| self.label = label | |
| def __repr__(self): | |
| return "<%s: %s>" % (self.__class__.__name__, self.label) | |
| class NFAState: | |
| """A state of a NFA, non deterministic finite automata. | |
| Attributes: | |
| target (rule_name): The name of the rule used to represent the NFA's | |
| ending state after a transition. | |
| arcs (Dict[Optional[str], NFAState]): A mapping representing transitions | |
| between the current NFA state and another NFA state via following | |
| a label. | |
| """ | |
| def __init__(self, rule_name): | |
| self.rule_name = rule_name | |
| self.arcs = [] | |
| def add_arc(self, target, label=None): | |
| """Add a new arc to connect the state to a target state within the NFA | |
| The method adds a new arc to the list of arcs available as transitions | |
| from the present state. An optional label indicates a named transition | |
| that consumes an input while the absence of a label represents an epsilon | |
| transition. | |
| Attributes: | |
| target (NFAState): The end of the transition that the arc represents. | |
| label (Optional[str]): The label that must be consumed for making | |
| the transition. If the label is not provided the transition is assumed | |
| to be an epsilon-transition. | |
| """ | |
| assert label is None or isinstance(label, str) | |
| assert isinstance(target, NFAState) | |
| self.arcs.append(NFAArc(target, label)) | |
| def __repr__(self): | |
| return "<%s: from %s>" % (self.__class__.__name__, self.rule_name) | |
| class DFA: | |
| """A deterministic finite automata | |
| A deterministic finite automata is a form of a finite state machine | |
| that obeys the following rules: | |
| * Each of the transitions is uniquely determined by | |
| the source state and input symbol | |
| * Reading an input symbol is required for each state | |
| transition (no epsilon transitions). | |
| The finite-state machine will accept or reject a string of symbols | |
| and only produces a unique computation of the automaton for each input | |
| string. The DFA must have a unique starting state (represented as the first | |
| element in the list of states) but can have multiple final states. | |
| Attributes: | |
| name (str): The name of the rule the DFA is representing. | |
| states (List[DFAState]): A collection of DFA states. | |
| """ | |
| def __init__(self, name, states): | |
| self.name = name | |
| self.states = states | |
| @classmethod | |
| def from_nfa(cls, nfa): | |
| """Constructs a DFA from a NFA using the Rabin–Scott construction algorithm. | |
| To simulate the operation of a DFA on a given input string, it's | |
| necessary to keep track of a single state at any time, or more precisely, | |
| the state that the automaton will reach after seeing a prefix of the | |
| input. In contrast, to simulate an NFA, it's necessary to keep track of | |
| a set of states: all of the states that the automaton could reach after | |
| seeing the same prefix of the input, according to the nondeterministic | |
| choices made by the automaton. There are two possible sources of | |
| non-determinism: | |
| 1) Multiple (one or more) transitions with the same label | |
| 'A' +-------+ | |
| +----------->+ State +----------->+ | |
| | | 2 | | |
| +-------+ +-------+ | |
| | State | | |
| | 1 | +-------+ | |
| +-------+ | State | | |
| +----------->+ 3 +----------->+ | |
| 'A' +-------+ | |
| 2) Epsilon transitions (transitions that can be taken without consuming any input) | |
| +-------+ +-------+ | |
| | State | ε | State | | |
| | 1 +----------->+ 2 +----------->+ | |
| +-------+ +-------+ | |
| Looking at the first case above, we can't determine which transition should be | |
| followed when given an input A. We could choose whether or not to follow the | |
| transition while in the second case the problem is that we can choose both to | |
| follow the transition or not doing it. To solve this problem we can imagine that | |
| we follow all possibilities at the same time and we construct new states from the | |
| set of all possible reachable states. For every case in the previous example: | |
| 1) For multiple transitions with the same label we colapse all of the | |
| final states under the same one | |
| +-------+ +-------+ | |
| | State | 'A' | State | | |
| | 1 +----------->+ 2-3 +----------->+ | |
| +-------+ +-------+ | |
| 2) For epsilon transitions we collapse all epsilon-reachable states | |
| into the same one | |
| +-------+ | |
| | State | | |
| | 1-2 +-----------> | |
| +-------+ | |
| Because the DFA states consist of sets of NFA states, an n-state NFA | |
| may be converted to a DFA with at most 2**n states. Notice that the | |
| constructed DFA is not minimal and can be simplified or reduced | |
| afterwards. | |
| Parameters: | |
| name (NFA): The NFA to transform to DFA. | |
| """ | |
| assert isinstance(nfa, NFA) | |
| def add_closure(nfa_state, base_nfa_set): | |
| """Calculate the epsilon-closure of a given state | |
| Add to the *base_nfa_set* all the states that are | |
| reachable from *nfa_state* via epsilon-transitions. | |
| """ | |
| assert isinstance(nfa_state, NFAState) | |
| if nfa_state in base_nfa_set: | |
| return | |
| base_nfa_set.add(nfa_state) | |
| for nfa_arc in nfa_state.arcs: | |
| if nfa_arc.label is None: | |
| add_closure(nfa_arc.target, base_nfa_set) | |
| # Calculate the epsilon-closure of the starting state | |
| base_nfa_set = set() | |
| add_closure(nfa.start, base_nfa_set) | |
| # Start by visiting the NFA starting state (there is only one). | |
| states = [DFAState(nfa.name, base_nfa_set, nfa.end)] | |
| for state in states: # NB states grow while we're iterating | |
| # Find transitions from the current state to other reachable states | |
| # and store them in mapping that correlates the label to all the | |
| # possible reachable states that can be obtained by consuming a | |
| # token equal to the label. Each set of all the states that can | |
| # be reached after following a label will be the a DFA state. | |
| arcs = {} | |
| for nfa_state in state.nfa_set: | |
| for nfa_arc in nfa_state.arcs: | |
| if nfa_arc.label is not None: | |
| nfa_set = arcs.setdefault(nfa_arc.label, set()) | |
| # All states that can be reached by epsilon-transitions | |
| # are also included in the set of reachable states. | |
| add_closure(nfa_arc.target, nfa_set) | |
| # Now create new DFAs by visiting all posible transitions between | |
| # the current DFA state and the new power-set states (each nfa_set) | |
| # via the different labels. As the nodes are appended to *states* this | |
| # is performing a breadth-first search traversal over the power-set of | |
| # the states of the original NFA. | |
| for label, nfa_set in sorted(arcs.items()): | |
| for exisisting_state in states: | |
| if exisisting_state.nfa_set == nfa_set: | |
| # The DFA state already exists for this rule. | |
| next_state = exisisting_state | |
| break | |
| else: | |
| next_state = DFAState(nfa.name, nfa_set, nfa.end) | |
| states.append(next_state) | |
| # Add a transition between the current DFA state and the new | |
| # DFA state (the power-set state) via the current label. | |
| state.add_arc(next_state, label) | |
| return cls(nfa.name, states) | |
| def __iter__(self): | |
| return iter(self.states) | |
| def simplify(self): | |
| """Attempt to reduce the number of states of the DFA | |
| Transform the DFA into an equivalent DFA that has fewer states. Two | |
| classes of states can be removed or merged from the original DFA without | |
| affecting the language it accepts to minimize it: | |
| * Unreachable states can not be reached from the initial | |
| state of the DFA, for any input string. | |
| * Nondistinguishable states are those that cannot be distinguished | |
| from one another for any input string. | |
| This algorithm does not achieve the optimal fully-reduced solution, but it | |
| works well enough for the particularities of the Python grammar. The | |
| algorithm repeatedly looks for two states that have the same set of | |
| arcs (same labels pointing to the same nodes) and unifies them, until | |
| things stop changing. | |
| """ | |
| changes = True | |
| while changes: | |
| changes = False | |
| for i, state_i in enumerate(self.states): | |
| for j in range(i + 1, len(self.states)): | |
| state_j = self.states[j] | |
| if state_i == state_j: | |
| del self.states[j] | |
| for state in self.states: | |
| state.unifystate(state_j, state_i) | |
| changes = True | |
| break | |
| def dump(self, writer=print): | |
| """Dump a graphical representation of the DFA""" | |
| for i, state in enumerate(self.states): | |
| writer(" State", i, state.is_final and "(final)" or "") | |
| for label, next in sorted(state.arcs.items()): | |
| writer(" %s -> %d" % (label, self.states.index(next))) | |
| def dump_graph(self, writer): | |
| """Dump a DOT representation of the DFA""" | |
| writer('digraph %s_dfa {\n' % self.name) | |
| for i, state in enumerate(self.states): | |
| writer(' %d [label="State %d %s"];\n' % (i, i, state.is_final and "(final)" or "")) | |
| for label, next in sorted(state.arcs.items()): | |
| writer(" %d -> %d [label=%s];\n" % (i, self.states.index(next), label.replace("'", '"'))) | |
| writer('}\n') | |
| class DFAState(object): | |
| """A state of a DFA | |
| Attributes: | |
| rule_name (rule_name): The name of the DFA rule containing the represented state. | |
| nfa_set (Set[NFAState]): The set of NFA states used to create this state. | |
| final (bool): True if the state represents an accepting state of the DFA | |
| containing this state. | |
| arcs (Dict[label, DFAState]): A mapping representing transitions between | |
| the current DFA state and another DFA state via following a label. | |
| """ | |
| def __init__(self, rule_name, nfa_set, final): | |
| assert isinstance(nfa_set, set) | |
| assert isinstance(next(iter(nfa_set)), NFAState) | |
| assert isinstance(final, NFAState) | |
| self.rule_name = rule_name | |
| self.nfa_set = nfa_set | |
| self.arcs = {} # map from terminals/nonterminals to DFAState | |
| self.is_final = final in nfa_set | |
| def add_arc(self, target, label): | |
| """Add a new arc to the current state. | |
| Parameters: | |
| target (DFAState): The DFA state at the end of the arc. | |
| label (str): The label respresenting the token that must be consumed | |
| to perform this transition. | |
| """ | |
| assert isinstance(label, str) | |
| assert label not in self.arcs | |
| assert isinstance(target, DFAState) | |
| self.arcs[label] = target | |
| def unifystate(self, old, new): | |
| """Replace all arcs from the current node to *old* with *new*. | |
| Parameters: | |
| old (DFAState): The DFA state to remove from all existing arcs. | |
| new (DFAState): The DFA state to replace in all existing arcs. | |
| """ | |
| for label, next_ in self.arcs.items(): | |
| if next_ is old: | |
| self.arcs[label] = new | |
| def __eq__(self, other): | |
| # The nfa_set does not matter for equality | |
| assert isinstance(other, DFAState) | |
| if self.is_final != other.is_final: | |
| return False | |
| # We cannot just return self.arcs == other.arcs because that | |
| # would invoke this method recursively if there are any cycles. | |
| if len(self.arcs) != len(other.arcs): | |
| return False | |
| for label, next_ in self.arcs.items(): | |
| if next_ is not other.arcs.get(label): | |
| return False | |
| return True | |
| __hash__ = None # For Py3 compatibility. | |
| def __repr__(self): | |
| return "<%s: %s is_final=%s>" % ( | |
| self.__class__.__name__, | |
| self.rule_name, | |
| self.is_final, | |
| ) |