こんにちは Antlr

2007年3月7日

HelloSablecc と言った後、Java スペース向けの別のコンパイラコンパイラであるAntlrも試してみたくなりました。このエントリと同様に、これは非常にシンプルな「ハローワールド」スタイルの文法で Antlr を動かすことだけが目的です。

SableCCと同様に、Antlrはコンパイラコンパイラツールです。しばらく前から存在しており、それを使用しているいくつかのプロジェクトに出会いました。SableCC(および由緒ある lex/yacc コンボ)とは異なり、LL 文法を使用して再帰下降パーサーを生成します。コンパイラの専門家は、LL または LALR のどちらが良いかについて議論したがりますが、ここではその議論には立ち入りません。

私の簡単な例は、次のような項目のリストのファイルを解析することです。

item camera
item laser

各行には、「item」キーワードの後に、項目の名前を表す単語が1つ続きます。各項目オブジェクトを、それらをすべてまとめる構成オブジェクトにロードします。

public class Configuration {
  private Map<String, Item> items = new HashMap<String, Item>();
  public Item getItem(String key) {
    return items.get(key);
  }
  public void addItem(Item arg) {
    items.put(arg.getName(), arg);
  }
public class Item {
  private String name;
  public Item(String name) {
     this.name = name;
   }

これが、上記で示したファイルを使用したテストです。

 @Test public void readTwoItems() {
    Reader input = null;
    try {
      input = new FileReader("catalog.txt");
    } catch (FileNotFoundException e) {
      throw new RuntimeException(e);
    }
    Configuration config = ParserCommand.parse(input);
    assertNotNull(config.getItem("camera"));
    assertNotNull(config.getItem("laser"));
    assertEquals(2, config.getItems().size());
  }

以前と同様に、この問題にコンパイラコンパイラを使用するのは馬鹿げていますが、コンソールに「hello world」と出力するのも同様です。新しい環境で常に「hello world」と書くのと同じ理由で、実際に何かを始める前に、すべてが機能することを確認するために、非常にシンプルなものを書くのが好きです。

このようなコンパイラコンパイラを使用する際の1つの面倒な点は、ビルドプロセスが複雑になることです。パーサーの Java クラスを作成するために、文法ファイルで antlr を実行し、コンパイルにそれらを含める必要があります。したがって、ant と戦う時が来ました。これが ant ターゲットです。

  <property name = "dir.parser" value = "${dir.gen}/parser"/>
  <path id = "path.antlr">
    <fileset dir = "${dir.lib}">
      <include name = "antlr*.jar"/>
      <include name = "stringtemplate*.jar"/>
    </fileset>
  </path>
  <target name = "gen" >
    <mkdir dir="${dir.parser}"/>
    <java classname="org.antlr.Tool" classpathref="path.antlr" fork = "true" failonerror="true">
      <arg value="-o"/>
      <arg value="${dir.parser}"/>
      <arg value="Catalog.g"/>
     </java>
  </target>

これにより、gen ディレクトリにコードが生成されます。このように、生成されたコードは、私が自分で書くソースコードとは分離されています。別のターゲットがコンパイルを行います。

 <property name = "dir.build" value = "classes/production/antlrLair"/> 
 <target name = "compile" depends = "gen">
    <mkdir dir="${dir.build}"/>
    <javac destdir="${dir.build}" classpathref="classpath">
      <src path = "src"/>
      <src path = "${dir.gen}"/>
      <src path = "test"/>
    </javac>
  </target>

最後に、ターゲットを使用してテストを実行できます。

<target name = "test" depends="compile">
    <junit haltonfailure = "on">
      <formatter type="brief"/>
      <classpath refid = "classpath"/>
      <batchtest todir="${dir.build}" >
        <fileset dir = "test" includes = "**/*Test.java"/>
      </batchtest>
     </junit>
   </target>

Antlr は、文法ファイル Catalog.g で動作します。文法ファイルは、文法の生成規則と、パーサーが生成規則に遭遇したときに実行するアクションを定義します。文法ファイルは、字句解析器も定義します(必要に応じて分割できます)。この意味で、Antlr は SableCC よりも伝統的(かつ柔軟)です。SableCC はアクションを許可せず、代わりに解析ツリーまたは AST を生成し、それを Java でウォークします。Antlr は任意のアクションを許可するか、SableCC と同じ方法でツリーの構築をサポートします。(Antlr はツリーをウォークするためにも文法ファイルを使用します。) 私は項目と構成のシンプルなドメインモデルを構築しているので、ツリー構築を避け、すべてのアクションで作業を行います。

このファイルをチャンクに分けて、説明を加えながら進めていきます。まずは、文法の見出しから始めます。

grammar Catalog;

Antlr は、生成されたパーサーにコードを挿入できるいくつかのポイントをサポートしています(SableCC が行うスーパークラスの生成の代わりに)。パッケージ宣言とインポートをヘッダーに入れます。

@header{
package parser;
import model.*;
}
@lexer::header {
package parser;
}

次のコード挿入は、生成されたクラスの本体にコードを配置することです。基本的には、これによりクラスにメンバーが追加されます。したがって、コマンドの名前が付けられています。

@members {
  public Configuration result = new Configuration();
}

これで、文法の生成規則に入ることができます。トップダウンパーサーであるため、トップダウンで行います。まず、カタログは複数の item 句の後にファイルの終わりが続くと言います。

catalog :  item* EOF;

次に、item 句を、リテラル文字列「item」の後に文字列が続くものとして定義します。

item 	: 'item'  name=STRING 
   {result.addItem(new Item ($name.text));};

ここにはアクションも入れます。アクションは、文字列の値に設定された名前を持つ新しい項目をモデル内に作成することです。中かっこ内のコードは、その項が認識された後にパーサーに追加される Java コードです。プロダクション内の要素に名前を付けて、アクションでそれらを参照できます。ここでは、文字列に「name」という名前を付けました。これは、ぎこちない書き方ではあるものの、コンテキストでは意味があります。

最後の生成規則は、文字列と空白の字句解析要素を定義します。

STRING 	: ('a'..'z' | 'A'..'Z')+ ;
WS : (' ' |'\t' | '\r' | '\n')+ {skip();} ;

空白のアクションは、スキップ(無視)することです。

Antlr は SableCC よりも使いやすい点がいくつかあります。Antlr には、IntelliJ にプラグインできるAntlrWorksという優れた IDE があります。このツールを使用すると、文法要素の構文強調表示と補完、文法の構文図のプロット、および解析するテストフラグメントの入力(結果の解析ツリーを表示)を行うことができます。これは、パーサーが何をしているかを確認するのに非常に役立つツールです。ただし、アクション内のコードの強調表示/補完はありません。これは理解できる苦痛です。

Antlr のもう 1 つの優れた機能は、現在製作中のまともな書籍が存在することです。この書籍では、ツールの動作方法と、言語およびコンパイラの原則に関する有用な背景について詳しく説明しています。この本は、完全な言語に取り組んでおり、コードを生成することを前提としています。これは、DSL の作業では必ずしも当てはまりません。ただし、与えられている詳細は、私がさらに深く調査する際に非常に貴重になると思われます。

Antlr のアクションは、モデルにデータを入力したい場合に簡単な方法のように思えます。中間解析ツリーまたは AST がここでどれほど役に立つかはわかりません。繰り返しますが、さらに調査することで、より良い感触が得られるでしょう。言語が複雑になるほど、中間ツリー表現を使用することがより役立ちます。私は、アクションまたは変換を伴うツリー構築を実行できる Antlr の柔軟性が好きです。

必然的に、この簡単な例でも問題が発生しました。私の最大の障害は、最初にカタログ項を catalog : item*; と定義したことです。つまり、EOF がありませんでした。その後、パーサーが偽の入力(xitem foo など)を受け取ってもエラーを示さなかったため混乱しました。これは、Antlr と AntlrWorks の間の不一致によって助けられませんでした(後者はエラーを示し、古いバージョンの AntlrWorks は空白を異なる方法で処理していました)。

(トラブルのもう 1 つの大きな原因は、ant と JUnit を機能させることでした。特に悪名高い「Ant はタスクまたはこのタスクが依存するクラスを見つけることができませんでした」というメッセージで、クラスパスの問題を診断しようとして費やした時間の量を考えたくありません。)