bit

ANTLR v4 の文法ファイルのサンプル

最近までいろいろ ANTLR v4 をいじっていて、ようやく文法ファイルの書き方がわかってきた。
でも初心者向けの解説記事を書けるほど頭の中がこなれていないので、サンプルだけ示すことにする。

こんなファイルを考える。

"This"
 "is
   a "
 "\"quoted\" text."
""

このファイルから、「This」「is<改行> a 」「"quoted" text.」「(空文字)」という四つの文字列を認識するパーサを書きたいとする。

ファイル解析のプログラムはスクラッチからがりがり書いてもよいけど、文法が複雑になってくるとプログラムが汚くなりがちである。そこで、構文解析プログラムジェネレータである ANTLR が便利ということになる。

ANTLR 自体の細かい説明は面倒なのでしないけど、要は文法ファイルを与えるとその文法に従ったファイルを解釈するための Java プログラムを生成してくれる。
といった流れは簡単なのだが、文法ファイルの書き方はそれほど初心者には優しくない。

文法ファイルは、字句解析部分と構文解析部分の二つの部分から成り立つが、最近はこれを個別のファイルとして書くのが流行り(?)である。

まず字句解析の文法ファイル StringLexer.g4

lexer grammar StringLexer;

QUOTE: '"' -> pushMode(STRING_MODE) ;
WS: ( ' ' | '\t' ) -> skip;
NL : ( '\r' '\n'? | '\n') -> skip ;
OUTER_CHAR: . -> skip ;

mode STRING_MODE;
RQUOTE: '"' -> popMode ;
ESC: '\\' -> skip, pushMode(ESCCHAR_MODE) ;
NOT_ESCCHAR: . ;

mode ESCCHAR_MODE;
ESCCHAR: ( '\\' | '"' ) -> popMode ;

「lexer」の行はおまじない。
最初の「mode」より上のグローバル部分と、二つの「mode」で、グローバル部分を一つのモードと考えると、合計三つのモードから構成される。
それで、グローバルの上のほうにある「QUOTE」の行では、引用符(") を読んだら、STRING_MODE というモードに移行する、という意味であり、STRING_MODE の最初の「RQUOTE」は、引用符を読み込んだら元のモードに戻るという意味である。

STRING_MODE は引用符の中を処理する部分であり、ESCCHAR_MODE はさらにその中のエスケープ文字を処理する部分である。まあ、あとは説明しなくても分かるでしょ。
エスケープ可能な文字はここでは2種類 (\と")に限定しているが、字句解析の段階でエラーにすると現場プログラム的にはエラー処理が面倒になるので、任意文字にしてもよいかもしれない。

この mode はとても便利である。字句解析ルールは何もしないとグローバルに効いてしまうので、その場合、字句解析プログラムは文字列の中だろうがコメントだろうが、お構いなしに字句解析ルールを当てはめてしまう。
実は最近まで mode を知らなかったので、このグローバル対策のためのバッドノウハウの塊みたいな文法ファイルを書いていたが、mode を使うときれいに字句解析文法ファイルを書ける。素晴らしい。
ただし mode は字句解析文法部分にしか使えないので、必然的に字句解析と構文解析とで文法ファイルを分ける必要が出てくる。

次に構文解析の文法ファイル。

StringParser.g4

parser grammar StringParser;
options { tokenVocab=StringLexer; }

strings
  : string*
  ;

string returns [ String text ]
  :
  QUOTE v=string_text RQUOTE {
    $text = $v.text;
  }
  ;

string_text
  :
  ( ESCCHAR
  | NOT_ESCCHAR
  )*
  ;

options のところが、さっきの字句解析ファイルのトークンを使っているよという宣言。
string ルールのところの RQUOTE の後の部分が、アクションというやつである。何もしないと 「"This"」と左右の引用符がくっついたままの文字列しかプログラムから取得できず面倒になるので、string_text のところだけを取り出して $text という変数に返すようにしている。

二つの文法ファイルができたらあとは、ANTLRに食わせて Java コードを生成させて、動かすだけである。

java -classath antlr-4.2.2-complete.jar org.antlr.v4.Tool -o <出力先> -package<パッケージ> StringLexer.g4 StringParser.g4

Java からの呼び出し方は以下の通り:

public void test(String filepath) throws IOException {
  ANTLRFileStream fromFileStream = new ANTLRFileStream(filepath);
  StringLexer lexer = new StringLexer(fromFileStream);
  CommonTokenStream tokens = new CommonTokenStream(lexer);

  StringParser parser = new StringParser(tokens);
  StringsContext stringsContext = parser.strings();
  for (StringParser.StringContext stringContext : stringsContext.string()) {
    System.out.println("[[" + stringContext.text + "]]");
  }
}

出力結果。

[[This]]
[[is
   a ]]
[["quoted" text.]]
[[]]