Java
DSL
antlr
antlr4

ANTLR4.7.1を利用したDSLの開発

まえがき

今回、自分が作っているJava製コード自動生成ツール、Damascusを利用しているユーザーから、自動生成用のテンプレートを利用するだけでなく、ソースコードからテンプレートを生成させて、カスタムテンプレートを作成したいという要望があがりました。

それを実装するのに、正規表現だけではうまく処理できず、解決方法を探していたところで、コンパイラ・コンパイラのANTLRを利用して、簡単なDSLを作成することで解決しました。

試行錯誤の末、DSLを実装できたので、今回の自分の開発フローを晒してみます。サンプルプロジェクトとして、今回は拙作のDamascusを利用します。

ANTLRの基本動作を理解する

ANTLRは処理系(プログラミング言語)を自作するためのJava製ツール。拡張BNFで記述されたファイル*.g4から、字句解析器(Lexer)と構文解析器(Parser)を自動生成してくれます。

右も左もわからない自分が参考にしたのは、ANTLRの作者であるTerence Parr教授の書いた解説本「The Definitive ANTLR 4 Reference」です。(以下ANTLR本) ページ数は300ページ近くと大分分厚いですが、平易な英語で書かれており、本文中に豊富なコード例と、Githubにサンプルコードがあるので、それを見ながら実際に動かしてみて、自分は2週間ほどでコードサンプルを触りながら読了できました。

最初はサンプルコードだけ見て色々いじってたのですが、結局よくわからず、最初からANTLR本を読むのが結局、一番早道だと思いました。WebサイトやAntlrのGithubリポジトリにも情報が色々ありますが、断片的で、全てがカバーできているわけでないので、ANTLR本を最初に全章、目を通すことを強くおすすめします。

開発環境

普段はIntelliJを利用しているのですが、EclipseのAntlrプラグインが一番使える感じでした。なので自分は

  • Lexer / Parser部分の開発にはEclipse + Antlrプラグイン
  • それ以外の部分の開発にはIntelliJ

という形で同じプロジェクトを2つのIDEで開いて開発していました。2018年1月現在、Oxygenが最新バージョンなので、それでのAntlrプラグインのインストール方法は以下です。

  1. Help -> Eclipse Market placeで、Antlrを検索。
  2. ANTLR 4 IDE 0.3.6をインストール
  3. インストール後、Eclipseが再起動
  4. 上部メニューから、Window -> Show view -> others -> ANTLR 4
  5. Parse Tree と Syntax Diagramの両方を選択。
  6. Parse TreeをEclipse内で開く(ペインが表示されるはず)
  7. DamascusをプロジェクトとしてEclipse内にインポートして、DmscSrcParser.g4を開きます。
  8. DmscSrcParser.g4内、一番上のfileルールをダブルクリックするとParse TreeペインにDmscSrcParser::fileと表示され、ブロックダイアグラムが表示される

開発フロー

Lexer / Parser設計

Damascusの中に、DmscSrcParser.g4DmscSrcLexer.g4という2つのファイルがあります。以下のようなサイクルで開発していました。

Lexer設計

拡張BNF記法を用いて、インストールしたAntlrプラグインのSyntax Diagramを確認しながらDmscSrcLexer.g4にトークン定義を作成しました。構文が不正だとエラー表示がされます。0からトークン定義を作るのは難しいので、ANTLR本に載っていたサンプルを参考に、今回の場合はANTLR本でいう島言語(Island Language)をタグを用いて作成する予定だったので、ModeTagsLexer.g4などを参考にして、ベースを作成しました。

Parser設計

上記で設計したトークン定義を利用して、DmscSrcParser.g4に構文解析ルールを定義しました。最初からうまくルールを定義することは難しいと思いますが、Parserで非常に複雑なルールを定義しなければいけない、もしくはうまくルールが適用されないという状況で行き詰まってしまったら、Lexerのトークンの定義を見直してみてください。Lexerがしっかり設計できていれば、Parserは簡潔になるはずです。

ここでは微調整を繰り返しながら、ANTLR本のサンプルを参考に、試行錯誤を繰り返す必要があるかと思います。最初から大きなルールを作るのは難しいので、今回の島言語であれば、DSLのスタートタグなのか、そうじゃないかの判定をまずできるような小さなルールを作成、その動作がうまくいったら、タグの中のアトリビュートのルールのテスト、というように小さいルールを局所的にテストしながら、それを組み合わせて大きなルールを作る、という意識がポイントかと思います。

DmscSrcParser.g4内の最上位ルール(Damascusの場合はfile)をクリックし、対象となる構文をペーストしてみて、正しくParse Treeにブロックダイアグラムに分解されるか目視で確認していきます。また、構文エラーを見ながら、Parser / Lexerを調整していきます。

Listener開発

LexerとParserがほぼ正しく構文を処理できるようになってきたら、今度はListenerを実装します。ANTLRではListner方式と、Visitor方式のインターフェース・実装を出力できます。Visitor方式はインタプリタのような逐次処理向きですが、今回は一括で処理させるので、Listnerを利用して開発をしました。

Damascusbuild.gradlegenerateGrammarSourceタスクを定義してあり、これを利用してプロジェクトルートでgradle generateGrammarSourceを実行すると、*.g4ファイルからLexer / Parserを生成します。生成されたDmscSrcParserBaseListenerを継承して、実装を設計していきます。

この段階まできたら、ユニットテストを実装していきます。JUnitでもいいのですが、Spockというテストフレームワークが柔軟にテストが構築できるので、DamascusではSpockのテストを利用しています。

まとめ

LexerとPaserの設計が一番難しく、とくにPaser設計をしている中で、Lexerの設計がまずく、何度もLexerを書き換えることで結果的にParserがシンプルになる、ということを学びました。ANTLRで生成される構文解析エンジンを利用することで、正規表現では処理しきれないような複雑な構文も容易に扱えるようになるのは大きな驚きでした。

ポエムっぽい記事になってしまいましたが、どなたかの役に立てたら幸いです!

Tips

Lexerを変更した際にうまく再読み込みしてくれない場合

EclipseのAntlrプラグインはよく動いてくれるのですが、Lexerを頻繁に変更した際に、変更した構文をうまく読み込んでくれないことがありました。以下の方法で解決していたので、参考までに書いておきます。

  1. gradle generateGrammarSourceを実行、そのあとgradle eclipseを実行
  2. まだ動きがおかしい場合は、*.g4があるディレクトリ(/src/main/antlr)でantlr4 DmscSrcLexer.g4 ; antlr4 DmscSrcParser.g4 ; javac Dms*.javaを実行、生成された*.tokens*.interpsrc/main/java/com/liferay/damascus/antlr/templateにコピーして、gradle eclipseを実行、DmscSrcParser.g4fileをダブルクリックして、Parse Treeペインを再確認する。
  3. それでもダメな場合は、Eclipse再起動をしてみてください。