def apply_action_to_stack(self, stack: Tuple[nltk.Nonterminal],
                            action: int) -> Tuple[nltk.Nonterminal, ...]:
    """Expand `stack` with the rule corresponding to `action`.

    Args:
      stack:  A sequence of `nltk.Nonterminal`s representing the current stack.
      action:  An action corresponding to a rule.

    Returns:
      A _new_ stack representing the result of applying the production
      corresponding to `action` to the top of `stack`.  If this is invalid
      (because the top of `stack` != the lhs of production), raises an error.
    """
    non_terminal_on_top_of_stack = stack[-1]
    stack_remainder = tuple(stack[:-1])

    production = self.grammar.productions()[action]
    if production.lhs() != non_terminal_on_top_of_stack:
      raise ValueError(
          f'Expected rule that expands {non_terminal_on_top_of_stack}, got: '
          f'{production}, stack={stack}')

    # Return the stack that results by replacing the top symbol with all the
    # non-terminals on the rhs of the applied rule, _in reverse order_.
    return stack_remainder + tuple(
        rhs for rhs in reversed(production.rhs())
        if nltk.grammar.is_nonterminal(rhs))
Expand `stack` with the rule corresponding to `action`.

    Args:
      stack:  A sequence of `nltk.Nonterminal`s representing the current stack.
      action:  An action corresponding to a rule.

    Returns:
      A _new_ stack representing the result of applying the production
      corresponding to `action` to the top of `stack`.  If this is invalid
      (because the top of `stack` != the lhs of production), raises an error.
    
language/search_agents/muzero/state_tree.py