presto-parser/src/main/java/io/prestosql/sql/parser/ErrorHandler.java/ErrorHandler/process
class ErrorHandler:        private Set<Integer> process(ParsingState start, int precedence)
        {
            Set<Integer> endTokens = new HashSet<>();

            // Simulates the ATN by consuming input tokens and walking transitions.
            // The ATN can be in multiple states (similar to an NFA)
            Deque<ParsingState> activeStates = new ArrayDeque<>();
            activeStates.add(start);

            while (!activeStates.isEmpty()) {
                ParsingState current = activeStates.pop();

                ATNState state = current.state;
                int tokenIndex = current.tokenIndex;
                boolean suppressed = current.suppressed;

                while (stream.get(tokenIndex).getChannel() == Token.HIDDEN_CHANNEL) {
                    // Ignore whitespace
                    tokenIndex++;
                }
                int currentToken = stream.get(tokenIndex).getType();

                if (state.getStateType() == RULE_START) {
                    int rule = state.ruleIndex;

                    if (specialRules.containsKey(rule)) {
                        if (!suppressed) {
                            record(tokenIndex, specialRules.get(rule));
                        }
                        suppressed = true;
                    }
                    else if (ignoredRules.contains(rule)) {
                        // TODO expand ignored rules like we expand special rules
                        continue;
                    }
                }

                if (state instanceof RuleStopState) {
                    endTokens.add(tokenIndex);
                    continue;
                }

                for (int i = 0; i < state.getNumberOfTransitions(); i++) {
                    Transition transition = state.transition(i);

                    if (transition instanceof RuleTransition) {
                        RuleTransition ruleTransition = (RuleTransition) transition;
                        for (int endToken : process(new ParsingState(ruleTransition.target, tokenIndex, suppressed, parser), ruleTransition.precedence)) {
                            activeStates.push(new ParsingState(ruleTransition.followState, endToken, suppressed, parser));
                        }
                    }
                    else if (transition instanceof PrecedencePredicateTransition) {
                        if (precedence < ((PrecedencePredicateTransition) transition).precedence) {
                            activeStates.push(new ParsingState(transition.target, tokenIndex, suppressed, parser));
                        }
                    }
                    else if (transition.isEpsilon()) {
                        activeStates.push(new ParsingState(transition.target, tokenIndex, suppressed, parser));
                    }
                    else if (transition instanceof WildcardTransition) {
                        throw new UnsupportedOperationException("not yet implemented: wildcard transition");
                    }
                    else {
                        IntervalSet labels = transition.label();

                        if (transition instanceof NotSetTransition) {
                            labels = labels.complement(IntervalSet.of(Token.MIN_USER_TOKEN_TYPE, atn.maxTokenType));
                        }

                        // Surprisingly, TokenStream (i.e. BufferedTokenStream) may not have loaded all the tokens from the
                        // underlying stream. TokenStream.get() does not force tokens to be buffered -- it just returns what's
                        // in the current buffer, or fail with an IndexOutOfBoundsError. Since Antlr decided the error occurred
                        // within the current set of buffered tokens, stop when we reach the end of the buffer.
                        if (labels.contains(currentToken) && tokenIndex < stream.size() - 1) {
                            activeStates.push(new ParsingState(transition.target, tokenIndex + 1, false, parser));
                        }
                        else {
                            if (!suppressed) {
                                record(tokenIndex, getTokenNames(labels));
                            }
                        }
                    }
                }
            }

            return endTokens;
        }