CX5 SOFTWARE

C++でANTLRを使う


目次

1. はじめに
2. ANTLRWorksでのグラマーファイルの記述
2.1. グラマーファイルのひな形の生成
2.2. 字句解析ルールの記述
2.3. 字句解析ルールのまとめ
2.4. 字句解析ルールの確認
2.5. 構文解析
3. C言語ソースコードの生成
3.1. ANTLRのインストール
3.2. C言語向けANTLRラインタイムのビルド
4. 属性とアクション
4.1. アクション
4.2. 属性
4.3. 特別なアクション
4.3.1. @lexer::header, @parser::header
4.3.2. @lexer::includes, @parser::includes
4.3.3. @lexer::members, @parser::members
4.3.4. @init, @after
4.3.5. ターゲット言語がC言語におけるアクションの使い方
5. 変数とスコープ
5.1. ルールの引数と戻り値
5.2. 動的スコープ
6. サンプル1
6.1. グラマーファイル
6.2. ヘッダファイル
6.3. アクションを追加したグラマーファイル
6.4. メインファイル
6.5. 動作確認
7. 最後に

表の一覧

2.1. 字句解析ルールのまとめ

第1章 はじめに

C++で開発中のアプリケーションで、簡単な定義ファイルを解析する必要があり、どうしようかと考えていました。安易な方法としては、自分で正規表現などを使いながら解析する方法ですが、 余計な空白や改行などの削除も考慮しようとすると、行いたいことの割りに結構大変です。XMLで定義するのが簡単ですがあまり読みやすくない上、コンパクトに定義を記述できないという問題があります。

なので、字句解析器(lexer)、構文解析器(parser)を使って解析をすることとしました。C/C++で使える字句解析器、構文解析器としては以下のようなものがあると思います。

  • Bison/Flex (C言語)

  • Boost Spirit (C++)

  • ANTLR (C言語)

Boost SpiritはC++で動作するということで最初に試してみたのですが、C++で構文を記述するという部分が逆に分かりにくくなっているように思えてやめました。Bison/FlexとANTLRのどちらにするかで迷った末、ANTLRで試してみることとしました。(特例があるとはいえBisonがGPLなのが少しひっかかったので。)

ANTLR (ANother Tool for Language Recognition)はLL構文解析を用いたパーサジェネレータです。ANTLR自身はJavaで記述されていますが、Java、C、C#、Objective-Cなどさまざまな言語向けのパーサを生成することができます。

ANTLRは、内部では以下のように入力テキストを処理しています。意識しなければならないのは、字句解析器と構文解析器の2つに分かれている点です。Bison/Flexの場合、Flexが字句解析を、Bisonが構文解析を受け持ちますが、ANTLRの場合、両方をANTLRで受け持ちます。 構文解析のルールを記述したグラマーファイル(.g)も一つにまとまっていますので、意識していないと混乱することになります。

作業の流れとしては、以下のようになります。

  • グラマーファイルの記述 - ANTLRWorksを用います。

  • グラマーファイルへのアクションの追加 - C++のコードをグラマーファイルへ追加します。

  • 生成されたパーサーを呼び出す処理の記述 - 生成されたパーサーを呼び出すメイン部です。

なお、動作確認はWindows上でVisual C++ 2010で行っています。

第2章 ANTLRWorksでのグラマーファイルの記述

2.1. グラマーファイルのひな形の生成

グラマーファイルは単なるテキストファイルですので、テキストエディタで記述することができます。ですが、ANTLRWorksというツールを用いると、グラマーの記述、確認を容易に行うことができます。 ANTLRWorksはこちらからダウンロードできます。ANTLRWorksはJava上で動作するので、あらかじめJava実行環境をインストールしてください。 Java実行環境がインストールされていれば、ダウンロードしたantlrworks-1.4.2.jarをダブルクリックすれば起動するはずです。

ANTLRWorksが起動したら、メニュー「File」-「New...」を選択します。すると生成するファイルの種類を選択するダイアログが表示されるので「ANTLR 3 Grammar (*.g)」を選択します。

すると、以下のようなダイアログが表示されます。まず、Grammar Nameにグラマー名を入れます。ここで入れた「名前+.g」がファイル名となります。次にTypeは「Combined Grammar」を選択し、字句解析と構文解析の両方を一つのファイルに記述する設定にします。

その下にあるLexical Itemが、あらかじめ含めておきたい字句解析の項目を指定する部分です。最初の内はすべてにチェックすることをおすすめします。もし不要になったら、後でいつでも削除できますので。

OKボタンを押すと、ひな形となるグラマーファイルが生成され、表示されます。

2.2. 字句解析ルールの記述

先ほども触れたようにグラマーファイルには字句解析と構文解析の両方のルールが含まれます。どのルールがどちらの定義となるかは、識別子が大文字で始まるか、小文字で始まるかで決まります。識別子が大文字で始まる定義は字句解析のルールとなり、小文字で始まる定義は構文解析のルールとなります。

自動生成された字句解析ルールの中で、INT(整数)の定義を見てみると以下のようになっています。

INT :	'0'..'9'+ ;

まず、字句解析ルールの識別子は大文字で始まらなければならないので「INT」とすべて大文字の識別子を用いています。コロン(:)の後に実際のルールが記述されており、「'0'..'9'+」となっています。 ここで「..」は、その範囲の文字を指します。ですので今回の場合'0'から'9'までの文字を指します。 次に、末尾の「+」は、前のルールの1回以上の繰り返しを意味します。つまり、全体としては、'0'から'9'までの数字の1回以上の繰り返しをINTと定義していることとなります。

次に、もう少し複雑な「ID」の定義を見てみましょう。

ID : ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')* ;

ANTLRでは、あるルールがあり、そのルールの後続のルールを記述したい場合は、単純にその順番にルールを並べて記述することとなります。 IDの場合、最初のルール「('a'..'z'|'A'..'Z'|'_')」の後に、二つ目のルール「('a'..'z'|'A'..'Z'|'0'..'9'|'_')*」が続くこととなります。

最初のルール「('a'..'z'|'A'..'Z'|'_')」において、「|」はルールのどれかが選択されることを表します(つまり、OR)。 例えば「'A' | 'B'」と定義した場合、'A'というテキスト、もしくは、'B'というテキストにマッチするルールとなります。 今回の場合は、「('a'..'z'|'A'..'Z'|'_')」となっているので、アルファベットの小文字、大文字、アンダースコアのどれかにマッチすることとなります。

次のルール「('a'..'z'|'A'..'Z'|'0'..'9'|'_')*」では、新しく「*」が出てきていますが、これは0回以上の繰り返しを表します。

つまり、全体としては、最初の一文字目がアルファベットの小文字、大文字、アンダースコアではじまり、2文字目以降がアルファベットの小文字、大文字、数字、アンダースコアとなる、1文字以上のテキストをIDと定義しています。

3つ目のルールとしてFLOATを見てみましょう。

FLOAT
    :   ('0'..'9')+ '.' ('0'..'9')* EXPONENT?
    |   '.' ('0'..'9')+ EXPONENT?
    |   ('0'..'9')+ EXPONENT
    ;
    
fragment
EXPONENT : ('e'|'E') ('+'|'-')? ('0'..'9')+ ;

こちらは、ルールFLOATからルールEXPONENTが参照される形で定義されています。まず、FLOATから見てみると、新しく出てきているのは「?」ですが、これは、前のルールがオプション、つまりあってもなくてもよいということを意味します。今回の場合EXPONENTつまり指数部はオプションであることを指定しています。

次にEXPONENTですが、定義自体はすでに見てきた内容の延長です。唯一異なるのがEXPONENTの前にあるfragmentです。fragmentは、そのルールが他のルールから参照される場合にのみ有効となるルールであることを意味しています。 つまり、このルール単独ではマッチしないことを意味します。今回の場合EXPONENTは"e10"や"e213"などにマッチしますが、これらのテキストは前に数値があった場合に意味があり、単独の"e10"を指数部と認識するのは困ります。 実際、"e100"などは変数名などに使われる可能性がありますので、本来ならばIDにマッチして欲しいものとなります。fragmentを付けることで、ルールFLOATを経由してのみEXPONENTが使われるようになります。

一応、生成されたグラマーファイル全体を以下に載せておきます。

grammar T;

ID  :	('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')*
    ;

INT :	'0'..'9'+
    ;

FLOAT
    :   ('0'..'9')+ '.' ('0'..'9')* EXPONENT?
    |   '.' ('0'..'9')+ EXPONENT?
    |   ('0'..'9')+ EXPONENT
    ;

COMMENT
    :   '//' ~('\n'|'\r')* '\r'? '\n' {$channel=HIDDEN;}
    |   '/*' ( options {greedy=false;} : . )* '*/' {$channel=HIDDEN;}
    ;

WS  :   ( ' '
        | '\t'
        | '\r'
        | '\n'
        ) {$channel=HIDDEN;}
    ;

STRING
    :  '"' ( ESC_SEQ | ~('\\'|'"') )* '"'
    ;

CHAR:  '\'' ( ESC_SEQ | ~('\''|'\\') ) '\''
    ;

fragment
EXPONENT : ('e'|'E') ('+'|'-')? ('0'..'9')+ ;

fragment
HEX_DIGIT : ('0'..'9'|'a'..'f'|'A'..'F') ;

fragment
ESC_SEQ
    :   '\\' ('b'|'t'|'n'|'f'|'r'|'\"'|'\''|'\\')
    |   UNICODE_ESC
    |   OCTAL_ESC
    ;

fragment
OCTAL_ESC
    :   '\\' ('0'..'3') ('0'..'7') ('0'..'7')
    |   '\\' ('0'..'7') ('0'..'7')
    |   '\\' ('0'..'7')
    ;

fragment
UNICODE_ESC
    :   '\\' 'u' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT
    ;

2.3. 字句解析ルールのまとめ

表2.1 字句解析ルールのまとめ

ルール意味
A | B | CA, B, Cのどれか('R' | 'G' | 'B')
A*Aの0回以上の繰り返し'0'*
A+Aの1回以上の繰り返し'0'+
A?Aがオプション(あってもなくてもよい)'0'+
~AA以外にマッチ~'\n'

2.4. 字句解析ルールの確認

ANTLRWorksを用いて字句解析ルールの確認(テスト)を行う方法を説明します。

まず、Interpreterタブを選択します。次に、下図において「ID」が表示されているプルダウンから、テストしたい字句解析ルールを選択します。

次に、プルダウンの下の部分にテストするテキストを入力します(下図では「ABC」と入力)。最後にプルダウンの左側にある矢印を押します。すると、右側に解析結果が表示されます。「ABC」は正しい「ID」のため、「ID」と表示されます。

もし「ID」として正しくない入力、例えば以下の場合のように「1ABC」と入れた場合は、(わかりにくいですが)Exceptionが表示されます。

この手順を他のルールに対しても行えば、一通りの確認を行うことができます。

2.5. 構文解析

次に、構文解析(parser)を取り上げます。といっても、字句解析とあまり大きくは変わりません。先ほども言ったように、構文解析の場合は、識別子の先頭文字が小文字でなければなりません。

例えば、ひな形に含まれていた字句解析ルールFLOATとSTRINGを用いて、リテラル(literal)という構文解析ルールを定義するには以下のようにします。

literal : FLOAT | STRING ;

第3章 C言語ソースコードの生成

3.1. ANTLRのインストール

ANTLRはコード生成ツールとラインタイムに大きくわかれていますが、それらが一式含まれているファイル(antlr-3.3.tar.gz)をこちらからダウンロードし、適当な場所に解凍します。なお、現時点での最新バージョンは3.3ですので、こちらを使いました。

コード生成ツールはJava上で動作しますので、Java実行環境をあらかじめインストールしておく必要があります。以下のようにすることで、コード生成ツールを起動することができます。(c:\opt\antlr-3.3\lib\antlr-3.3-complete.jarはantlr-3.3.tar.gzを解凍した場所に合わせて変更してください。)

$ java -classpath c:\opt\antlr-3.3\lib\antlr-3.3-complete.jar org.antlr.Tool
ANTLR Parser Generator  Version 3.3 Nov 30, 2010 12:45:30
usage: java org.antlr.Tool [args] file.g [file2.g file3.g ...]
  -o outputDir          specify output directory where all output is generated
  -fo outputDir         same as -o but force even files with relative paths to dir
  -lib dir              specify location of token files
  -depend               generate file dependencies
  -report               print out a report about the grammar(s) processed
  -print                print out the grammar without actions
  -debug                generate a parser that emits debugging events
  -profile              generate a parser that computes profiling information
  -trace                generate a recognizer that traces rule entry/exit
  -nfa                  generate an NFA for each rule
  -dfa                  generate a DFA for each decision point
  -message-format name  specify output style for messages
  -verbose              generate ANTLR version and other information
  -make                 only build if generated files older than grammar
  -version              print the version of ANTLR and exit.
  -X                    display extended argument list 

通常は、グラマーファイル(.g)を引数に指定することで、ソースコードが生成されます。例えば、T.gというグラマーファイルからソースコードを生成するには以下のようにします。

$ java -classpath c:\opt\antlr-3.3\lib\antlr-3.3-complete.jar org.antlr.Tool T.g

上記のコマンドを実行すると、(ターゲットがC言語の場合)以下のファイルが生成されます。

  • T.tokens

  • TLexer.c

  • TLexer.h

  • TParser.c

  • TParser.h

生成されるのはC言語のソースコードですが、内容はC++コンパイラでもコンパイルできるものです。ですので、我々が記述するソースコードはC++でも問題ありません。Visual C++でC++のソースファイルとしてコンパイルさせるため、TLexer.cとTParser.cのファイル名を*.cppに変更します。

$ ren TLexer.c TLexer.cpp
$ ren TParser.c TParser.cpp

ターゲット言語の指定ですが、これは、グラマーファイルに以下の記述を追加することで指定できます。

grammar T;
options {
    language = C;
}

ターゲット言語をJava言語にするならJavaを、C言語にするならばCを指定します。今回の場合は、C言語をターゲットとしますので、「language = C」と指定します。

3.2. C言語向けANTLRラインタイムのビルド

antlr-3.3.tar.gzを解凍したディレクトリ「antlr-3.3/runtime/C」にC言語向けのランタイムのソースファイルが含まれています。Visual C++でコンパイルするには、ディレクトリ内にあるC.slnを開き、ビルドを実行するだけです(スタティックLIBおよびDLLのデバッグ版とリリース版)。Unix系の場合は、「./configure; make; make install」を実行してください。

第4章 属性とアクション

字句解析ルールと構文解析ルールを書けば、あるテキストがそのルールに一致しているかどうかを確認することができます。 しかし、多くの場合、解析した結果から、インタプリタのように何か処理を実行したり、別形式のファイルに変換したりするのが普通だと思います。そのためには、属性とアクションを知る必要があります。

4.1. アクション

アクションはルールに「{}」で囲んで、ターゲット言語を直接記述します。「ターゲット言語」はグラマーファイルのlanguageオプションに指定した言語で、今回の場合であれば、C/C++のコードを記述することとなります。

test: 'ABC' { アクション }

この場合、'ABC'という文字列にマッチするルールですが、これにマッチするとアクションが実行されることになります。例えば、以下のグラマー定義では、'ABC'にマッチすると標準出力に文字を出力します。

test: 'ABC' { printf("match\n"); }

4.2. 属性

字句解析および構文解析で一致したトークンは、あらかじめANTLRが用意している属性を持っています。属性にはいろいろあるのですが、一番よく使うのがtextという属性で、マッチした文字列を取得することができます。さらにターゲット言語がC言語の場合、text->charsとすることで文字列ポインタ(char*/wchar_t*)を受け取ることができます。(ただし、text->charsの型はpANTLR3_UINT8というANTLRの独自型になっているのでキャストが必要)

例えば、以下のようにすることでマッチした文字列を標準出力に表示することができます。

test: INT { printf("match %s\n", $INT.text->chars); }

上記の場合、INTはひとつしかないので$INT.textでテキスト属性を取得することができます。ですが、例えば、以下のように2つのINTで座標を表す場合はどうでしょうか。

position: '(' INT ',' INT ')' { ... }

最初がxで、次がyであるとし、それぞれを別々にアクション内で参照するにはラベルを付けます。ラベルは、トークンの前に「ラベル名=」とすることで付けることができます。

position: '(' x=INT ',' y=INT ')' { printf("position (%s, %s)\n", $x.text->chars, $y.text->chars); }

上記の場合「x=INT」で、最初のINTにxというラベルを付けています。同様に、二番目のINTにyというラベルを付けています。

4.3. 特別なアクション

先ほど説明したアクションは、ルールを解析するためのC言語の関数内に出力されてしまいますので、ヘッダファイルのインクルードやクラス宣言などに使えません。このため、特別なアクションが用意されています。

4.3.1. @lexer::header, @parser::header

@lexer::header, @parser::headerは、生成される字句解析(lexer)および構文解析(parser)のヘッダファイル(.h)およびCファイル(.c)の先頭(ヘッダ)に出力されるアクションを記述することができます。注意としては、@headerはヘッダファイルとCファイルの両方に出力されますので、単純に定義を書いてしまうと二重定義になります。

@lexer::header {
class Position {
public:
    int x;
    int y;
};
}

@parser::header {
struct Color {
	float red;
	float green;
	float blue;
};
}
    			

4.3.2. @lexer::includes, @parser::includes

@lexer::includes, @parser::includesは、生成される字句解析(lexer)および構文解析(parser)のCファイル(.c)の先頭に追加されるアクションです。#includeで他のヘッダをインクルードしたり、Cファイル内だけで使われる関数などを記述します。

@lexer::includes {
#include <iostream>
#include <string>
#include <vector>
}

@parser::includes {
#include <iostream>
#include <string>
#include <vector>
}
    			

4.3.3. @lexer::members, @parser::members

@lexer::members, @parser::membersは、メンバということで、オブジェクト指向言語であればクラスのメンバとなるものを定義します。ですが、ターゲット言語がCの場合、当然ながら、生成されるコードはクラスではないので@includesと違いがありません。

4.3.4. @init, @after

@initと@afterは、各ルール定義に記述します。そして、そのルール定義の解析が行われる前に@initが呼び出され、解析の後に@afterが呼び出されます。主に、変数の宣言、初期化を@initに、変数などの後始末を@afterに記述します。

text
@init {
  char *p = new char[32];
}
@after {
  delete []p;
}
    : ID;

ちょっとわかりずらいですが、ルール名とコロン(:)の間に@initと@afterアクションを記述します。

4.3.5. ターゲット言語がC言語におけるアクションの使い方

アクションとそれらが生成されるヘッダーファイルとCファイルのどこに挿入されるのかを示したのが下図です。

ターゲット言語がJavaであれば、@header, @includes, @membersを使い分けて、生成されるクラスファイルを使いやすい形にすることになります。ですが、ターゲット言語がC言語の場合、単に生成される位置が異なるだけであまり違いがありません。

ですので、C++を使う場合、グラマーファイルに定義を記述せず、外部のインクルードファイル(.h)にすべての定義を記述する方法をお勧めします。そして、@headerにてそれをインクルードするようにします。(@includeではなく@headerを使っているのは、@includeの場合、その前にantlr3.hのインクルードが行われてしまい。そのためか、C++標準ライブラリヘッダと衝突しているのかコンパイルエラーとなるためです。) 注意として、@headerで記述された内容はヘッダファイルとCファイルの両方に出力されるので、定義が一度しか定義されないように#ifndef~#endifでガードする必要があります。

以下はインクルードファイルと@headerの使い方の例です。

// Color.h
#ifndef SAMPLE1_COLOR_H_
#define SAMPLE1_COLOR_H_
class Color {
public:
	float red;
	float green;
	float blue;
}
#endif
// T.g
grammar T;
@lexer::header {
#include "Color.h"
}
@parser::header {
#include "Color.h"
}

第5章 変数とスコープ

5.1. ルールの引数と戻り値

今まで説明をした内容でもグローバル変数を使えば、グラマー定義にアクションを記述して対象ファイルを解析することはできます。具体的には、@xxx::includesでグローバル変数を宣言して、ルールごとにアクションを追加し、その中でグローバル変数に解析結果を入れていきます。ただし、できればグローバル変数は使いたくないところです。

ANTLRでは、通常のC/C++の関数と同じように、各ルールに引数と戻り値を宣言することができます。これにより、あるルールから別のルールへ変数を渡すことができます。ルールに対して引数と戻り値を宣言するには、ルール定義に以下のように記述します。ここで、引数nおよび戻り値nは「型」と「ラベル」をスペースで区切って記述します。

rule1[引数1, 引数2, ..., 引数n] returns [戻り値1, 戻り値2, ..., 戻り値n] : ... ;

ここで注意するのは、C/C++と異なり戻り値が複数指定できる点です。

例えば、int型のxとyを受け取り、floatのr1, r2を返すルールrule1の定義は以下の通りとなります。

rule1[int x, int y] returns [float r1, float r2] : ... ;

このルールを呼び出す親ルールrule0は以下の通りとなります。

rule0: rule1[1,2] { $rule1.r1; };

rule1内のアクションではラベルxとyを用いて、渡された引数を参照することができます。呼び出し側のルールでは$rule1.r1と$rule1.r2というラベルで戻り値を参照できます。

5.2. 動的スコープ

先ほど説明した引数と戻り値による方法は、C/C++のスタックと同じように動作しますので、一見、すべてうまくいくように思えます。ですが、実際使ってみると、使いにくい点があるのがわかります。大きな理由は、グラマー定義は、ルール同士が複雑に参照されるように記述するのが一般的であるという点です。

例えば、下図において、rule1で設定した変数xをrule6に渡したいとします。この場合、間にあるrule2~rule5の引数として変数xを渡す必要があります。これが結構難しいのは容易に想像できます。

そこで、ANTLRには動的スコープというものを宣言できます。動的スコープは、あるルールとマッチしたときに動的スコープが有効になり、それ以降に呼び出されたルールではその動的スコープ内の変数を参照することができるという仕組みです。

例えば、下図の場合、rule1で動的スコープを開始していますので、結果としてrule2~rule6では動的スコープ内の変数にアクセスすることができます。

動的スコープを用いるには、まず、動的スコープの名前とその中に含まれる変数を宣言します。以下では、Xyzという動的スコープを宣言しています。

scope Xyz {
    std::vector<int> v;
    float a1;
    float a2;
}

次に、その動的スコープを使い始めるルールに、以下のような記述を追加します。

rule1
    scope Xyz;
    : ID { $Xyz::a1; } ;

ちょっとわかりずらいですが、ルール名rule1の後に「scope Xyz;」と記述している部分が、動的スコープの開始を指定しています。この後のルールでは「$Xyz::変数名」で動的スコープ内の変数にアクセスできます。

第6章 サンプル1

では実際に独自の定義を解析するプログラムを作成してみます。独自定義の書式ですが、以下のように複数の色をRGBで定義できるものとします。ここで色には名前を付けることができます。RGBの各値は0.0~1.0の小数点の数値とします。以下ではこの独自定義を色情報定義と呼ぶこととします。

rgb[名前](R値,G値,B値),...,rgb[名前](R値,G値,B値)

ここで示したサンプルのVisual C++ 2010プロジェクトはここ(antlr_sample1.zip)よりダウンロードできます。

6.1. グラマーファイル

グラマーファイルは以下の通りとなります。ANTLRWorksが生成した部分以外のルールとしてはcolorDefとcolorの2つとなります。ここでは、まだ何もアクションを追加していません。

grammar Color;

ID  :	('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')*
    ;

FLOAT
    :   ('0'..'9')+ '.' ('0'..'9')* EXPONENT?
    |   '.' ('0'..'9')+ EXPONENT?
    |   ('0'..'9')+ EXPONENT
    ;

fragment
EXPONENT : ('e'|'E') ('+'|'-')? ('0'..'9')+ ;

COMMENT
    :   '//' ~('\n'|'\r')* '\r'? '\n' {$channel=HIDDEN;}
    |   '/*' ( options {greedy=false;} : . )* '*/' {$channel=HIDDEN;}
    ;

WS  :   ( ' '
        | '\t'
        | '\r'
        | '\n'
        ) {$channel=HIDDEN;}
    ;

colorDef : color (',' color)* ;

color : 'rgb[' ID '](' FLOAT ',' FLOAT ',' FLOAT ')' ;

グラマーファイルを記述したら、ANTLRWorksで正しく解析できることを確認します。

6.2. ヘッダファイル

次に、色情報定義の解析結果を受け取るクラスColorとコンテナColorMapの定義をヘッダファイル(Color.h)に記述します。

#ifndef SAMPLE1_COLOR_H_
#define SAMPLE1_COLOR_H_

namespace sample1
{

class Color
{
public:
    float red;
    float green;
    float blue;
};

typedef std::unordered_map<std::wstring, sample1::Color> ColorMap;

inline float toFloat(pANTLR3_UINT8 chars)
{
    return (float)_wtof((const wchar_t *)chars);
}

} // namespace sample1

#endif // SAMPLE1_COLOR_H_

すべての定義はsample1という名前空間内に定義しています。クラスColorは色情報をRGBで保存するためのものです。色情報はColorMapというマップ(STLコンテナのunordered_map)に名前とのペアで保存されます。最後に、小数点の数値で指定された色値をfloat型に変換するためのtoFloatという関数を定義しています。

ここで、toFloatの引数に指定しているpANTLR3_UINT8という型ですが、これはANTLRのCランタイムが文字列を格納する型です。実際に格納されている内容は、利用する文字コードにより異なります。今回はUTF-16を用いますので、そのままwchar_t*にキャストしています。

6.3. アクションを追加したグラマーファイル

先ほど作成したグラマーファイルにアクションを追加します。

grammar Color;

options {
    language = C;
}

@lexer::header
{
#define ANTLR3_INLINE_INPUT_UTF16
}

@parser::header {
#include <iostream>
#include <string>
#include <unordered_map>
#include <antlr3.h>
#include "Color.h"
}

ID  :	('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')*
    ;

FLOAT
    :   ('0'..'9')+ '.' ('0'..'9')* EXPONENT?
    |   '.' ('0'..'9')+ EXPONENT?
    |   ('0'..'9')+ EXPONENT
    ;

fragment
EXPONENT : ('e'|'E') ('+'|'-')? ('0'..'9')+ ;

COMMENT
    :   '//' ~('\n'|'\r')* '\r'? '\n' {$channel=HIDDEN;}
    |   '/*' ( options {greedy=false;} : . )* '*/' {$channel=HIDDEN;}
    ;

WS  :   ( ' '
        | '\t'
        | '\r'
        | '\n'
        ) {$channel=HIDDEN;}
    ;

colorDef[sample1::ColorMap &colorMap] :	color[colorMap] (',' color[colorMap])*	;

color[sample1::ColorMap &colorMap] : 'rgb[' name=ID '](' red=FLOAT ',' green=FLOAT ',' blue=FLOAT ')' 
    {
        sample1::Color color;
        color.red = sample1::toFloat($red.text->chars);
        color.green = sample1::toFloat($green.text->chars);
        color.blue = sample1::toFloat($blue.text->chars);
        std::wstring name((wchar_t*)$name.text->chars);
        colorMap.insert(std::make_pair<std::wstring, sample1::Color>(name, color));
    };

まず、ターゲット言語がC言語であることを指定します。

options {
    language = C;
}

次に、入力文字コードとしてUTF-16を用いることを指定します。

@lexer::header
{
#define ANTLR3_INLINE_INPUT_UTF16
}

今回はアクションはすべてパーサー側に記述しますので、パーサーに必要なヘッダファイルがインクルードされるように指定します。

@parser::header {
#include <iostream>
#include <string>
#include <unordered_map>
#include <antlr3.h>
#include "Color.h"
}

ルールcolorDefでは、引数としてcolorMapを受け取るようにします。このcolorMapはC++のmain側から渡されることになります。さらに、ルールcolorにもcolorMapを渡します。

colorDef[sample1::ColorMap &colorMap] :	color[colorMap] (',' color[colorMap])*	;

ルールcolorが実際に解析とその結果の格納をしている部分です。

color[sample1::ColorMap &colorMap] : 'rgb[' name=ID '](' red=FLOAT ',' green=FLOAT ',' blue=FLOAT ')' 
    {
        sample1::Color color;
        color.red = sample1::toFloat($red.text->chars);
        color.green = sample1::toFloat($green.text->chars);
        color.blue = sample1::toFloat($blue.text->chars);
        std::wstring name((wchar_t*)$name.text->chars);
        colorMap.insert(std::make_pair<std::wstring, sample1::Color>(name, color));
    };

色情報(red, green, blue)からsample1::Colorオブジェクトを生成します。そして、名前(name)とペアにしてstd::unordered_mapに挿入しています。

6.4. メインファイル

最後に、生成されたパーサーを呼び出すメイン部(main.cpp)を作成します。

#include <stdio.h>
#include <tchar.h>

#include <iostream>
#include <string>
#include <unordered_map>
#include <antlr3.h>

#include "Color.h"
#include "ColorLexer.h"
#include "ColorParser.h"

#if defined(DEBUG) | defined(_DEBUG)
#pragma comment(lib, "antlr3cd.lib")
#else
#pragma comment(lib, "antlr3c.lib")
#endif

void parse(const std::wstring &inputStr, sample1::ColorMap &colorMap)
{
    pANTLR3_UINT8 input_string = (pANTLR3_UINT8)inputStr.c_str();
    ANTLR3_UINT32 size = static_cast<ANTLR3_UINT32>(inputStr.size() * 2);
    pANTLR3_INPUT_STREAM stream = antlr3StringStreamNew(
                                      input_string,
                                      ANTLR3_ENC_UTF16,
                                      size,
                                      (pANTLR3_UINT8)"color");
    pColorLexer lexer = ColorLexerNew(stream);
    pANTLR3_COMMON_TOKEN_STREAM tokenStream = 
        antlr3CommonTokenStreamSourceNew(ANTLR3_SIZE_HINT, TOKENSOURCE(lexer));
    pColorParser parser = ColorParserNew(tokenStream);

    parser->colorDef(parser, colorMap);

    stream->free(stream);
    lexer->free(lexer);
    tokenStream->free(tokenStream);
    parser->free(parser);
}

int _tmain(int argc, _TCHAR* argv[])
{
    std::wstring s(L"rgb[a1](1.0,0.0,0.0),rgb[a2](0.0,1.0,0.0)");
    sample1::ColorMap colorMap;
    parse(s, colorMap);

    for (auto it = colorMap.begin(); it != colorMap.end(); ++it) {
        sample1::Color &color = it->second;
        std::wcout << L"name=" << it->first
                   << L", Color[red=" << color.red
                   << L",green=" << color.green
                   << L",blue=" << color.blue << L"]"
                   << std::endl;
    }

    return 0;
}

parse関数が実際に字句解析(lexer)と構文解析(parser)を呼び出している部分となります。入力文字列をstd::wstringで渡すと、ColorMapに結果を入れて返します。

parse関数の1行目では、入力文字列をANTLRの文字列ポインタ型(pANTLR3_UINT8)に変換しています。

pANTLR3_UINT8 input_string = (pANTLR3_UINT8)inputStr.c_str();

2行目では、入力文字列のバイト数を設定しています。今回はUTF-16エンコードを用いているので、文字数を2倍にした値を設定しています。

ANTLR3_UINT32 size = static_cast<ANTLR3_UINT32>(inputStr.size() * 2);

3行目では、入力文字列より、文字列ストリーム(ANTLR3_INPUT_STREAM)を関数antlr3StringStreamNewを用いて生成しています。

pANTLR3_INPUT_STREAM stream = antlr3StringStreamNew(
                                      input_string,
                                      ANTLR3_ENC_UTF16,
                                      size,
                                      (pANTLR3_UINT8)"color");

第一引数は入力文字列、第二引数には入力文字列のエンコードを指定するのでUTF-16を指定しています。第三引数は入力文字列のバイト数を指定します。最後の第四引数はファイル名ですが、今回はファイルから読み込んでいないのでダミーの文字列を設定しています。

今回は文字列から定義を読み込んでいますが、ファイルから読み込む場合はantlr3StringStreamNewの代わりにantlr3FileStreamNewを用います。

字句解析器を構築します。この際、先ほど作成した文字列ストリームを引数として渡します。

pColorLexer lexer = ColorLexerNew(stream);

ここで型pColorLexerや関数ColorLexerNewの名前は、グラマー名Colorから自動的に決定されます。

次に、字句解析器によりトークンに分解されたので、これをトークンストリームとして受け取り、構文解析器への入力として渡します。

pANTLR3_COMMON_TOKEN_STREAM tokenStream = 
    antlr3CommonTokenStreamSourceNew(ANTLR3_SIZE_HINT, TOKENSOURCE(lexer));
pColorParser parser = ColorParserNew(tokenStream);

最後に、構文解析器を呼び出し、実際に構文解析を実行させます。この際、colorDefという関数を呼び出していますが、これは、グラマー定義上の最初に入力を処理するのがcolorDefルールだからです。

parser->colorDef(parser, colorMap);

6.5. 動作確認

以上ですべてのファイルの準備が完了しました。まず、グラマーファイルからLexerとParserを生成させます。

java -classpath C:\opt\antlr-3.3\lib\antlr-3.3-complete.jar org.antlr.Tool Color.g

以下のファイルが生成されます。

  • Color.tokens

  • ColorLexer.c

  • ColorLexer.h

  • ColorParser.c

  • ColorParser.h

*.cをC++として認識させるため拡張子を*.cppに変更します。

ren ColorLexer.c ColorLexer.cpp
ren ColorParser.c ColorParser.cpp

最後にすべてのファイルをコンパイルします。コンパイルが完了したら、生成された実行ファイルを実行すると以下のような結果が出力されます。

name=a1, Color[red=1,green=0,blue=0]
name=a2, Color[red=0,green=1,blue=0]

第7章 最後に

最後に、ANTLRを使う際の注意点を2つほど。

まず、ASTについてです。C言語をターゲット言語とした場合でも、ANTLRにAST(Abstract Syntax Tree)を構築させることはできます。ですが、C言語をターゲットとしていることから、生成されるツリーはC++のSTLコンテナを用いず、ANTLRで定義されている独自のコンテナが使われます。このため、これをC++に渡そうとすると、STLコンテナに詰め替える作業が必要となります。ですので、C++を使う場合には、ASTではなくアクションで直接STLコンテナに詰めていく方法をお勧めします。

次に、定義のグラマー(構文)を定義する際は、どこでどこまでの処理を行うかを常に意識しておくとよいと思います。ANTLRでは、字句解析、構文解析、アクション、メイン部(ANTLRを呼び出すC++アプリ)の4箇所でそれぞれ解析および操作をすることができます。字句解析、構文解析で解析できれば、ANTLRがすべて行ってくれるので楽ですが、これには定義のグラマーがそうなるように設計されている必要があります。

例えば、簡易プログラミング言語を作成する際、変数をPerlなどのように必ず「$」で始まるようにすれば字句解析でも変数と認識させることができます。これをC言語のように単純にアルファベットと数字の組み合わせにすると、関数名などの他のIDと区別することは字句解析では不可能です。

ですので、定義のグラマーを設計する際は、出来る限り早い段階で解析が行えるように工夫をすると、処理が楽になります。


CX5 SOFTWARE, 2011