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に関する復帰処理
    • 現在のfollowに=が含まれる(演算子コンテキスト)にもかかわらず、トークンの種類はexpressionのfirstに含まれる(式が来てる)場合、後続する式をスキップ
    • 現在のfollowに識別子が含まれる(式コンテキスト)にもかかわらず、トークンの種類はoperatorのfirstに含まれる(演算子が来てる)場合、後続する演算子をスキップ
    • エラーが連続しないように、式コンテキストの直前までトークンをスキップ(constraintとexpressionのfirst集合は同値)

ちょっと甘い感じはする。ただ、実用的にはこんなもんですかね。

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回出る
    • hoge.foo.bar (ERROR)
    • {@link java.lang.Object} (ERROR)
    • a = b
  • {@link java.lang.Object} までスキップしてしまうと、次の文法で困る
    • hoge.foo.bar {@link java.lang.Object} = aaa
      • スキップした場合: [ hoge.foo.bar {@link java.lang.Object} ] -> ERROR, [= aaa] -> ERROR
      • 現在の実装: [ hoge.foo.bar ] -> ERROR, [{@link java.lang.Object} = aaa] -> OK


ともあれ、もう少し調整が必要な感じ。