ANTLR(8) - Parserのエラー処理
- ルールの末尾に catch[ExceptionType ] { ... } と書くと、ルール特有のエラー処理を書ける
- 全体的にエラー処理に統一性がない。細かいことやるならすべて書き直し
- FIRST集合とFOLLOW集合はある程度自前で計算しなきゃならない
- エラー発生直後のFOLLOW集合はとれるけど、任意の地点のFOLLOW集合はとる方法がわからなかった。フィールドには存在するのにフィールドの命名規則がわからん…
- 通常のエラー処理では、mismatch発生後にFOLLOW集合に含まれる要素を発見するまでスキップする模様。カッコ多すぎとかならそれくらいで対応できるか。
いろいろ大変でした。Mini Irenka Query文法でconstraintを導出する付近でエラー処理を追加してみる。constraint解析中の任意の個所でエラーが発生したら、そのconstraintをできる限りスキップしてみるという感じです。
ASTの生成
ANTLR(7) - Lexerのエラー処理 - しげるメモ を部分的に書き換え。300行くらいになったので全部は貼らない。
@parser::rulecatch { catch (RecognitionException re) { throw re; } }
- 単に@rulecatchでよい。パーサのルールを囲むtry節のcatch (RecognitionException) を独自に設定する。
@parser::members { private static BitSet FIRST_EXPRESSION = new BitSet(); static { FIRST_EXPRESSION.add(NAME); FIRST_EXPRESSION.add(LINK_START); } private static BitSet FIRST_OPERATOR = new BitSet(); static { FIRST_OPERATOR.add(OP_EQUAL); FIRST_OPERATOR.add(OP_INCLUDE); }
- 単に@membersでよい。Parserのメンバ定義を開始。
- FIRST_hogehoge は気合でFIRST集合計算。これくらいどこかに持っていてくれても…と思ったけど、確かに普通は使わない
- ルールの先頭に来るトークン一覧です
private List<RecognitionException> exceptions; private int recovering; @Override public void reportError(RecognitionException re) { if (recovering > 0) { return; } if (exceptions == null) { exceptions = new ArrayList<RecognitionException>(); } // avoid reentrant else if (exceptions.get(exceptions.size() - 1) == re) { return; } exceptions.add(re); }
- 例外のレポート処理をフックして、例外情報をかき集めてる。
- lexerと同様。
@Override public void recoverFromMismatchedToken( IntStream in, RecognitionException e, int ttype, BitSet follow) throws RecognitionException { // single token deletion if (in.LA(2) == ttype) { reportError(e); beginResync(); in.consume(); endResync(); in.consume(); return; } else if (ttype == EOF) { reportError(e); beginResync(); while (in.LA(1) != EOF) { in.consume(); } endResync(); return; } else if (!recoverFromMismatchedElement(in, e, follow)) { throw e; } }
- ルール内で期待したトークンとストリームから流れてきたトークンが違う際の処理
- 通常処理(BaseRecognizer)だとSystem.errに無駄に出力してるので除去
- 次の3つの処理
- エラー時に、エラーの直後も同じトークンなら確実にまたエラーとなるだろうから、それら2つを殺す
- EOFが期待されてたらどうしようもないのでそこで終了
- FOLLOW集合に含まれる要素までスキップ
- @rulecatchしたはずなのにいろいろなところからまだ呼び出される模様
- トレースしたらそれほど害がなさそうなので残す
private void recoverConstraint(RecognitionException re) throws RecognitionException { if ( lastErrorIndex==input.index() ) { input.consume(); } lastErrorIndex = input.index(); BitSet followSet = computeErrorRecoverySet(); beginResync(); recovering++; try { // operator was expected, but expression if (followSet.member(OP_EQUAL) && FIRST_EXPRESSION.member(re.getUnexpectedType())) { expression(); } // expression was expected, but operator else if (followSet.member(NAME) && FIRST_OPERATOR.member(re.getUnexpectedType())) { operator(); expression(); } // next should be an expression while (!FIRST_EXPRESSION.member(input.LA(1))) { input.consume(); } } catch (RecognitionException nested) { throw re; } finally { recovering--; endResync(); } }
- constraintに関する復帰処理
ちょっと甘い感じはする。ただ、実用的にはこんなもんですかね。
public List<RecognitionException> getExceptions() { if (exceptions == null) { return Collections.emptyList(); } else { return Collections.unmodifiableList(exceptions); } } }
- @parser::membersの最後。これまでに溜まった例外の一覧を返す。
constraint : expression operator expression -> ^(CONSTRAINT operator expression expression) ; catch [RecognitionException re] { if (retval.tree == null) { retval.tree = (CommonTree)adaptor.nil(); } reportError(re); recoverConstraint(re); }
- constraintで失敗したら、復帰処理を呼ぶ
- retval.treeをnullにしておくと、rewrite ruleの処理で落ちる。バグか?
検証
public class Recovery1Test { public static void main(String[] args) throws IOException, RecognitionException { CharStream in = new ANTLRStringStream(...); Recovery1Lexer lexer = new Recovery1Lexer(in); CommonTokenStream tokens = new CommonTokenStream(lexer); Recovery1Parser parser = new Recovery1Parser(tokens); CommonTree root = parser.query().tree; System.out.println(root.toStringTree()); System.out.println("Lexer Errors:"); for (RecognitionException e: lexer.getExceptions()) { System.out.println(" " + e.toString()); } System.out.println("Parser Errors:"); for (RecognitionException e: parser.getExceptions()) { System.out.println(" " + e.toString()); } } }
- いつも通りパースしたあと、溜まってるエラーを出力してみる。
- ANTLRStringStreamは文字列をストリーム化するクラス
@when hoge.foo.bar = {@link java.lang.Object} a = b ↓ (QUERY (CONSTRAINT OP_EQUAL (. (. (PLACEHOLDER hoge) foo) bar) (DECLARED_TYPE (TYPE_NAME java lang Object))) (CONSTRAINT OP_EQUAL (PLACEHOLDER a) (PLACEHOLDER b))) Lexer Errors: Parser Errors:
- エラーがない文法は、当然そのまま動く
- 出力加工済み。
@when hoge.foo.bar = = {@link java.lang.Object} a = b ↓ (QUERY (CONSTRAINT OP_EQUAL (PLACEHOLDER a) (PLACEHOLDER b))) Lexer Errors: Parser Errors: NoViableAltException(11!=[224:1: primary_term : ( placeholder -> placeholder | '{@link' link '}' -> link );])
- = を二重にすると、その文はスキップされて a = b のみ解釈
@when hoge.foo.bar {@link java.lang.Object} a = b ↓ (QUERY (CONSTRAINT OP_EQUAL (PLACEHOLDER a) (PLACEHOLDER b))) Lexer Errors: Parser Errors: NoViableAltException(14!=[211:1: operator : ( '=' -> OP_EQUAL | 'in' -> OP_INCLUDE );]) NoViableAltException(16!=[211:1: operator : ( '=' -> OP_EQUAL | 'in' -> OP_INCLUDE );])
- = をなくすと、次の3制約と解釈して、エラーが2回出る
- {@link java.lang.Object} までスキップしてしまうと、次の文法で困る
ともあれ、もう少し調整が必要な感じ。