Java
antlr

antlr3→antlr4移行

More than 3 years have passed since last update.

antlr3からantlr4への移行

なんとかantlr3からantlr4に移行したので、変更点・違いなどをまとめておく。修正にあたっては、The Definitive ANTLR 4 Referenceも参考にした。antlr4の情報としてはまとまっているが、antlr3からの変更点にはあまり触れられていないので、対応するanltr4のコードを探すのが難しい。結局ググってStack Overflowで見つけた内容もあった。

1. ユニットテストを書く
antlr3→antlr4では色々と変わっていて実行時にエラーになる内容も多い。構文解析器周りはテストしやすいので、ユニットテストがあるとこうしたデグレードが見つかりやすい。実際、ユニットテストのおかげで細かい仕様変更にも追従できた。

2. @header@parser::header に変更。@members@parser::membersに変更。
antlr3では@header, @membersだったのがそれぞれ@parser::header, @parser::membersに変更になっているので修正する。@lexer::header, @lexer::members はそのまま。

3. 返り値がノード名と同じにできなくなったので修正
antlrでビルドすると

Expr.4g:22:19: return value expr conflicts with rule with same name

と言われることがある。これは次のように、ノード名と返り値で同じ名前を使っているからである。

Expr.g
expr returns [int expr]
    :   e=multExpr {$expr= $e.expr;}
        (   '+' e=multExpr {$expr += $e.expr;}
        |   '-' e=multExpr {$expr -= $e.expr;}
        )*
    ;

antlr3では問題なかったが、antlr4ではエラーになるので次のように別名に置き換える。

Expr.4g
expr returns [int expr2]
    :   e=multExpr {$expr2= $e.expr2;}
        (   '+' e=multExpr {$expr2 += $e.expr2;}
        |   '-' e=multExpr {$expr2 -= $e.expr2;}
        )*
    ;

4. | の動作が変わった

Expr.g
expr returns [int expr2]
    :   e=multExpr {$expr2 = $e.expr2;}
        (  ( op1 = '+' | ep2 = '-') e2 = multExpr )*
       {$expr2 = Operator.operate( $op1.text, $op2.text, $e2);}
    ;

は実行時にNullPointerExceptionを吐く。$op1, $op2 のどちらかがnullになるからである。これを書き換えて

Expr.4g
expr returns [int expr2]
    :   e=multExpr {$expr2= $e.expr2;}
        (  op1 = '+' e=multExpr {$expr2 = Operator.operate( $op1.text, null , $e2);}
        |  op2 = '-' e=multExpr {$expr2 = Operator.operate( null, $op2.text , $e2);}
        )*
    ;

と、明示的にnullを入れておく。以下未検証であるが、Operator.operateを修正して、

Expr.4g
expr returns [int expr2]
    :   e=multExpr {$expr2= $e.expr2;}
        ((  op1 = '+' | op2 = '-') e=multExpr {$expr2 = Operator.operate( $op1, $op2 , $e2);}
        )*
    ;

とtextの取り出しをOperator.oeprate()中で行うようにしても動作すると思われる。

5. 最短一致の (options{greedy=false;}: ~) を正規表現の最短マッチ ? に置き換える
antlr3では最短一致を表すときに、

Expr.g
        (options {greedy=false;}: . )*

を使っていたが、これが廃止され正規表現の最短マッチ ? を使うようになった

Expr.4g
       .*?

6. @init$ 追加
antlr3では@initでは変数名に$は不要であったが、antlr4では必要になった

Expr.g
expr returns [Expr expr2]
@init {
 expr2 = new Expr();
}

は以下のように変数名に$を付与する

Expr.4g
expr returns [Expr expr2]
@init {
 $expr2 = new Expr();
}

7. emitErrorMessage()は廃止。その代わり、lexer, paserにaddErrorListener()が追加されたのでこちらを使う。

antlr.g
@members {
  @Override
  public void emitErrorMessage(String message) {
    // do something
  }
}

を以下で置き換える。

ParserErrorListner.java
public class ParserErrorListner implements ANTLRErrorListener {
    @Override
    public void syntaxError(Recognizer<?, ?> recognizer,
            Object offendingSymbol, int line, int charPositionInLine,
            String msg, RecognitionException e) {
                // do somting
    }

    @Override
    public void reportAmbiguity(Parser arg0, DFA arg1, int arg2, int arg3,
            boolean arg4, BitSet arg5, ATNConfigSet arg6) {
        // TODO Auto-generated method stub
    }

    @Override
    public void reportAttemptingFullContext(Parser arg0, DFA arg1, int arg2,
            int arg3, BitSet arg4, ATNConfigSet arg5) {
        // TODO Auto-generated method stub
    }

    @Override
    public void reportContextSensitivity(Parser arg0, DFA arg1, int arg2,
            int arg3, int arg4, ATNConfigSet arg5) {
        // TODO Auto-generated method stub
    }
}

8. パーサの呼び出し方が変更

OldParser.java
    public static Statement createParser(String someString) {
        CharStream stream = new ANTLRStringStream(someString);
        Lexer lexer = new Lexer(stream);

        CommonTokenStream tokens = new CommonTokenStream(lexer);
        Parser parser = new Parser(tokens);

        Statement statement = parser.statement()
        return statement;
    }

を以下に置き換える。

Parser.java
    public static Statement createParser(String someString) {
        CharStream stream = new ANTLRInputStream(someString);
        Lexer lexer = new Lexer(stream);
        lexer.addErrorListener(new LexerErrorLisner());

        CommonTokenStream tokens = new CommonTokenStream(lexer);
        Parser parser = new Parser(tokens);
        parser.addErrorListener(new ParseErrorLisner());

        StatementCauseContext context = parser.statementCause();
        Statement statement = context.statement;
        return statement;
    }

9. import 文の変更
antlr4のruntimeライブラリにはv4が付いているので、これを修正する

OldParser.java
import org.antlr.runtime.ANTLRStringStream;
import org.antlr.runtime.CharStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;

を以下に置き換える

Parser.java
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.RecognitionException;

ユニットテストと生成されたソースコードが頼り

ググってもantlr3→antlr4移行ガイドのようなものが見つからなかったので手探りで移行した。頼りにしたのはユニットテストとantlrが生成したlexer, parserのソースコードである。特に構文解析に失敗するときの挙動はユニットテストがないと互換性を保つのが難しい。正常系だけでなく異常系のテストも予め作っておくとよい。