Takes an action in the current grammar state, returning a new grammar state with whatever
        updates are necessary.  The production rule is assumed to be formatted as "LHS -> RHS".

        This will update the non-terminal stack.  Updating the non-terminal stack involves popping
        the non-terminal that was expanded off of the stack, then pushing on any non-terminals in
        the production rule back on the stack.

        For example, if our current ``nonterminal_stack`` is ``["r", "<e,r>", "d"]``, and
        ``action`` is ``d -> [<e,d>, e]``, the resulting stack will be ``["r", "<e,r>", "e",
        "<e,d>"]``.

        If ``self._reverse_productions`` is set to ``False`` then we push the non-terminals on in
        in their given order, which means that the first non-terminal in the production rule gets
        popped off the stack `last`.

def take_action(self, production_rule: str) -> 'GrammarStatelet':
        """
        Takes an action in the current grammar state, returning a new grammar state with whatever
        updates are necessary.  The production rule is assumed to be formatted as "LHS -> RHS".

        This will update the non-terminal stack.  Updating the non-terminal stack involves popping
        the non-terminal that was expanded off of the stack, then pushing on any non-terminals in
        the production rule back on the stack.

        For example, if our current ``nonterminal_stack`` is ``["r", "<e,r>", "d"]``, and
        ``action`` is ``d -> [<e,d>, e]``, the resulting stack will be ``["r", "<e,r>", "e",
        "<e,d>"]``.

        If ``self._reverse_productions`` is set to ``False`` then we push the non-terminals on in
        in their given order, which means that the first non-terminal in the production rule gets
        popped off the stack `last`.
        """
        left_side, right_side = production_rule.split(' -> ')
        assert self._nonterminal_stack[-1] == left_side, (f"Tried to expand {self._nonterminal_stack[-1]}"
                                                          f"but got rule {left_side} -> {right_side}")

        new_stack = self._nonterminal_stack[:-1]

        productions = self._get_productions_from_string(right_side)
        if self._reverse_productions:
            productions = list(reversed(productions))

        for production in productions:
            if self._is_nonterminal(production):
                new_stack.append(production)

        return GrammarStatelet(nonterminal_stack=new_stack,
                               valid_actions=self._valid_actions,
                               is_nonterminal=self._is_nonterminal,
                               reverse_productions=self._reverse_productions)