KEIS BLOGは株式会社ケイズ・ソフトウェアが運営しています。

KEIS BLOG

Introduction to Antlr! Pt.3


shimoda01
こんにちは。最近、 SodaStream というソーダ生成機を手に入れて、自家製の激安ハイボール(ウィスキーをソーダで割ったもの)を求めて飽くなき探求を続けている下田です。この写真はその検証用に大人買いした水です。今では、 300ml の角ハイボールの原価が、80円くらいになりました。水道水にしたら30円でいけます。そう考えると、たまに見かけるハイボール 490円って結構高いなぁって思います。ちなみにハイボールってウィスキーのソーダ割りなので、角ハイボールって書いてなかったら別の激安ウィスキーが使われてることがあり、そういうものに限って次の日に残るものもあるので、気をつけてくださいね。話は完全にそれてしまいましたが、今回はまた Antlr について話を進めていきたいと思います。
さて、始めにいつものおまじない、と言いたいところですが、今回はクラスパス通すのが面倒になってきたので、 Gradle という Java のビルドツールを使って行きたいと思います。 Ant と大して変わりません。記述方法が Groovy っていう言語になったくらいですね。では、汎用的な Antlr 用のビルドタスクを作ります。

apply plugin: 'java'
apply plugin: 'eclipse'

sourceCompatibility = 1.8
version = '1.0'

repositories {
    mavenCentral()
}

configurations {
  antlr4 {
    description = "ANTLR4"
  }
}

dependencies {
    compile 'org.antlr:antlr4-runtime:4.5.1-1'
    antlr4 'org.antlr:antlr4:4.5.1-1'
}

sourceSets {
  main {
    java {
      srcDir 'src/main/java'
      srcDir 'src/generated-sources/java'
    }
  }
}

ext.antlr = [
  antlrSource: 'src/main/antlr'
  , destinationDir: "src/generated-sources/java"
  , grammarpackage: "a.antlr",
]

task antlrOutputDir << {
  mkdir(antlr.destinationDir)
}

task generateGrammarSource(dependsOn: antlrOutputDir, type: JavaExec) {
  description = 'Generates Java sources from ANTLR4 grammars.'

  inputs.dir file(antlr.antlrSource)
  //outputs.dir file(antlr.destinationDir)

  def grammars = fileTree(antlr.antlrSource).include('**/*.g4')

  main = 'org.antlr.v4.Tool'
  classpath = configurations.antlr4
  def pkg = antlr.grammarpackage.replaceAll("\\.", "/")
  args = ["-o", "${antlr.destinationDir}/${pkg}"/*, "-atn"*/, "-visitor", "-package", antlr.grammarpackage, grammars.files].flatten()

}

compileJava {
  dependsOn generateGrammarSource
  source antlr.destinationDir
}

clean {
  delete antlr.destinationDir
}

src/main/antlr というディレクトリを作って、そこに以前作成した MyMySQL.g4 というファイルを放り込んで、以下のコマンドを実行すれば、お手軽に Parser を生成できます。 IT の世界ってこういうのがあったらいいなっていうものは大体どこかに転がっています。本当に素晴らしい世界ですね!ちなみに、寝ててもお金が入るシステムはどこにも落ちてないか、まだ見つけられていません。誰か見つけたら教えて下さいね。

gradle clean generateGrammarSource

さて、準備が整ったところで、今回は、 Antlr の戻り値の機能を使って行きたいと思います。戻り値の機能ですが、以下の様な定義のグループがあるとします。

grammar ReturnTest;

tuple : StringLiteral (' ' StringLiteral)*;

StringLiteral: '\'' StringCharacters? '\'' ;
fragment StringCharacters: StringCharacter+ ;
fragment StringCharacter: ~['] | '\'\'' ;

WS : [ \t\r\n]+ -> skip;

このグループの戻り値を定義することが出来ます。 tuple がメソッドであるようなイメージで問題ありません。そのメソッドの中で、 Java のコードを直接記述することができるので、自分の好きなクラスのインスタンスを作成して戻り値として返すことも可能です。では、実際に戻り値を設定してみます。

tuple returns[List<String> list]: a=StringLiteral (' ' bs+=StringLiteral)*
  {
    List<String> alist = new ArrayList<>();
    alist.add($a.getText());
    for (Token b : $bs) {
      alist.add(b.getText());
    }
    $list = alist;
  }
;

一つずつ説明していきたいと思います。まずは、 tuple の隣りにある returns という部分。これは、戻り値の定義です。 Java と戻り値の定義とほとんど変わらないので、すっと理解できると思います。次に a= ですが、これは、変数定義です。 bs+= も同様に変数定義ですが、 += と記述するとリスト型になります。bs+= と書いてあるグループには、アスタリスクがあるので個数が、 0..n となります。なので、パースする入力値によっては、 0 から、複数個になるということですね。いつもの構文定義以降にある括弧は、この構文の子となる分岐全てが処理された後の処理と思っていただいて問題ありません。なので、 a, bs には、処理後のデータが入っている前提です。ここでややこしいのが、 a と bs は、 $ を使わないと参照できません。通常だと IDE サポートが無いので、何でコンパイルエラーになるんだろうとハマることも良くあります。ちなみに、 StringLiteral は、fragment なので、一律 Token というクラスが渡されるので、 Token というクラスに存在するメソッド、 getText を使って実際に入力されたデータを取得できます。取得したデータをそのままリストに詰め込み戻り値として設定しているのが主な処理となります。では、実際に、コンパイルしてテストクラスを作成し、実行してみましょう。

package a;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;

import a.antlr.ReturnTestLexer;
import a.antlr.ReturnTestParser;
import a.antlr.ReturnTestParser.TupleContext;

public class Main2 {
    public static void main(String[] args) {
        ReturnTestLexer lexer = new ReturnTestLexer(new ANTLRInputStream("'a' 'b' 'c'"));
        CommonTokenStream tokenStream = new CommonTokenStream(lexer);
        ReturnTestParser parser = new ReturnTestParser(tokenStream);
        TupleContext tuple = parser.tuple();
        tuple.list.stream().forEach(System.out::println);
    }
}

このテストクラスでは、「’a’ ‘b’ ‘c’」という文字列を先ほど作成した Parser に入力し結果を出力しているだけです。
よく見ると、 TupleContext というクラスに先ほど戻り値として設定した変数名 list がクラス変数として参照できるところに注目してください。実際に実行した結果は以下のようになります。

'a'
'b'
'c'

いかがでしょうか、自分の想定していた通りの結果が出ましたか。ちなみに、コンピューターの世界で言う tuple とは、 immutable な list のことを指します。ちなみにここで使っている list は immutable ではありませんので、最期に Guava というライブラリを使って immutable な list にしてみましょう。

@parser::header {
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
}

tuple returns[List<String> list]: a=StringLiteral (' ' bs+=StringLiteral)*
  {
    Builder<String> builder = ImmutableList.<String> builder();
    builder.add($a.getText());
    for (Token b : $bs) {
      builder.add(b.getText());
    }
    $list = builder.build();
  }
;

straightforward な書き方です。他のクラスを使いたい時は、header に import を追加するだけで、参照が可能になります。以上で今回の記事は終わりたいと思います。ここまでで、 Antlr の基本的な使い方はマスターできたと思います。次回はさらに別の仕組みと融合させていきたいと思います。

【関連記事】
Splunkに株価を取り込んでみた ft. Fujikawa
Introduction to Antlr!
Introduction to Antlr! Pt.2
JavaPoetで簡単コード生成!