public function parse(string $input, $actions = []): TreeNodeInterface
    {
        if ($actions instanceof ActionsMap) {
            $actions_map = $actions;
        } elseif ($actions) {
            $actions_map = new ActionsMap($actions);
        } else {
            $actions_map = null;
        }

        $stack = new Stack($this->table, $actions_map);

        $pos = 0;
        $token = null;
        try {
            while (true) {
                while ($stack->getStateRow()->isReduceOnly()) {
                    $stack->reduce();
                }

                $expected_terms = array_keys($stack->getStateRow()->terminalActions);
                $match = $this->grammar->parseOne($input, $pos, $expected_terms);

                if ($match) {
                    /** @var Token $token */
                    $token = $match->token;
                    $pos = $match->nextOffset;
                    $symbol_name = $token->getType();
                } else {
                    $token = null;
                    $symbol_name = null;
                }

                while (true) {
                    if ($token) {
                        $terminal_actions = $stack->getStateRow()->terminalActions;
                        if (isset($terminal_actions[$symbol_name])) {
                            $stack->shift($token, $terminal_actions[$symbol_name]);
                            goto NEXT_SYMBOL;
                        }
                    }
                    if ($stack->getStateRow()->eofAction) {
                        if ($token) {
                            throw new UnexpectedInputAfterEndException(
                                $this->dumpTokenForError($token),
                                $token->getOffset()
                            );
                        }
                        goto DONE;
                    }
                    $stack->reduce();
                }

                NEXT_SYMBOL:
            }
            DONE:
        } catch (AbortParsingException $e) {
            throw new AbortedException($e->getMessage(), $e->getOffset(), $e->getPrevious());
        } catch (NoReduceException $e) {
            // This unexpected reduce (no rule to reduce) may happen only
            // when current terminal is not expected. So, some terminals
            // are expected here.

            $expected_terminals = [];
            foreach ($stack->getStateRow()->terminalActions as $name => $_) {
                $expected_terminals[] = Symbol::dumpType($name);
            }

            throw new UnexpectedTokenException(
                $this->dumpTokenForError($token),
                $expected_terminals,
                $token ? $token->getOffset() : strlen($input)
            );
        } catch (StateException $e) {
            throw new InternalException('Unexpected state fail', 0, $e);
        }
        $tokens_gen = null;

        return $stack->done();
    }