JavaCC Tips

最近は、XMLの普及によって、パーサージェネレータの出番はめっきり少なくなりましたが、ちょっとした独自形式ファイルの読み取りや、数式の解釈にはまだまだ役に立つツールです。ここでは、自分が引っかかったケースや、おそらく他の人も役に立つメモを適当に綴っていきます。この方面の専門家ではないので間違いなどありましたらお知らせ下さい。

JavaCC?

コンパイラーのような、ある決まった形式のファイルを読んで、何かをするプログラムを自動生成するツール。コンパイラーを作るためのコンパイラー。単純作業だけど作ると大変な「パースする(文を読んで解釈する)プログラム」を作ってくれるソフト。<(説明も難しかったり。)

開発元:JavaCC@java.net

インストールと簡単なチュートリアルなど

JavaTipやSmartDocで有名な浅海さんのページが参考になります。

リンクなど

JavaCCの使い方
有用なメモが盛りだくさんの「Java覚え書き」による、インストールとチュートリアル
JavaCCで日本語を扱う方法
JavaHouse MLで流れたJavaCCで日本語を扱うための方法
JavaCC、構文解析ツリー、およびXQueryの文法 (1), (2)
developerWorksの記事
JavaCC Grammar Repository
SQLやJavaScriptなどのいろいろな文法がおいてあります。
The JacaCC FAQ
英語ですが、つまづきそうなポイントがおさえられています。

JavaCCの使いどころ

ぜひ使いたい場面

このような時には手軽にプログラムを書くことが出来ます。

JavaCC文法があれば、幸せな人が増えるみたいです。

使うと苦労する場面

ファイルや文字列を読んで解釈するプログラムを書きたいときに、このようなツールを使うわけですが、自動生成だけに次のようなケースには使いづらかったりします。

異なる文法が混在する場合
JavaCCではこのような、柔軟な解釈がやりづらいです。
場面によって、異なる意味を持つ文字や単語がある場合
上と同じようなケースです。例えば、ひとつのファイルの中で、カンマ「,」を文字の区切りに使ったり、ただの文字列として扱ったりする場合がこのケースに含まれます。ただし、文法が複雑でなければ何とかなります。
日本語のように、単語の切れ目が自明でない場合
JavaCCは英語のような文法を仮定しているので、空白などで区切ってない文法は難しいです。しかし、これも頑張れば何とかできます。

私の経験上、以上のケースに一致する場合は、JavaCCを使って文法解釈するのに大変苦労します。苦労しないことがJavaCCの存在意義だとすれば、別の方法を考えることも検討した方がいいかもしれません。

Tips

トークンについて

まずJavaCCを使う上でやることは、トークン(Token)と呼ばれる文法の最小要素を定義することです。(JavaCCによって生成された文法解釈プログラムは、入力された文書を、このトークンという単位に分解してから解釈を始めますので、これが無ければ始まりません。)

トークンには以下の4種類ありますが、普通は上の2つだけで大丈夫です。

SKIP
文字通り、無視して読み飛ばされるもの。空白、タブ、改行文字など。
TOKEN
正規表現や、他のTOKENを使って定義する文法の最小単位。
MORE
トークン分解中に何か作業をしたりできるらしい。
SPECIAL_TOKEN
コメント文とか、文法に関係無くどこにでも現れる可能性のある特殊なトークン

トークンの例などはJavaCCに付属のサンプルにたくさん付いています。大変参考になります。

日本語文字列の表現は?

JavaCCのサンプルにある、JavaGrammarを参考にして、

  < #LETTER: //日本語を含んだ文字
      [
       "\u0024",
       "\u0041"-"\u005a",
       "\u005f",
       "\u0061"-"\u007a",
       "\u00c0"-"\u00d6",
       "\u00d8"-"\u00f6",
       "\u00f8"-"\u00ff",
       "\u0100"-"\u1fff",
       "\u3040"-"\u318f",
       "\u3300"-"\u337f",
       "\u3400"-"\u3d2d",
       "\u4e00"-"\u9fff",
       "\uf900"-"\ufaff"
      ]
  >

を使うとか、あるいは単純に、

 < #NONASCII: ["\u0080"-"\ufaff"] >

を使うという手もあるかと思います。なお、日本語を入力するには、後述の細工を行う必要があります。

正規表現で表現できないトークンの分割

いろいろあるかと思いますが、ここではTeXの数式の例を出します。

TeXの数式を解釈する場合、「xy_123^45\beta」は「 x | y | _ | 1 | 23 | ^ | 4 | 5 | \beta 」と分解する方法が自然です。

しかし、普通に文法を考えると、「xy | _ | 123 | ^ | 45 | \beta」と文字が続いてしまい、全然意味の違うものとして解釈されてしまいます。 それではと、単語や数字を1文字単位で区切ってしまうと、「12.34」や、「\text{this is a pen.}」などの別の個所で問題が起きてしまう上に、誰も1文字単位で文法を考えたくありません。

これを解決するには、「x y_1 23^4 5\beta」のように、入力文字列の適当な個所に空白を入れます。つまり、JavaCCによる文法解釈プログラムに入れる前に、自前のプリプロセッサによる前処理を行うということです。これは文法解釈よりも簡単ですから、ちょっと頑張れば解決します。しかし、あまり複雑な前処理をやってしまうと、何のためのJavaCCか分かりませんから、JavaCCの作業との兼ね合いで一番簡単になる解を見つけることが重要です。

InputStreamではなくて、文字列を読ませたい

StringInputStreamなるクラスを作る

前は、StringBufferInputStreamというクラスがあったのですが、今は Deprecated なので自分で作ります。単に、InputStreamでStringReaderをラップするだけです。

public class StringInputStream extends InputStream {
    StringReader in;
    private StringInputStream() {}

    /** build input stream from given string.
     * @param source input stream source
     */
    public StringInputStream(String source) {
	in = new StringReader(source);
    }

    public int read() throws IOException { return in.read(); }
    
    public void close() throws IOException { in.close(); }

    public synchronized void mark(int readlimit) {
	try {
	    in.mark(readlimit);
	} catch(IOException e) {
	    throw new RuntimeException("IOException : StringInputStream["+
				       toString()+"]");
	}
    }
 
    public synchronized void reset() throws IOException { in.reset(); }
    public boolean markSupported() { return true; }
}

JavaCCの文法解釈クラスのコンストラクタで、以下のように与えれば動きます。

    public MyParser(String in) throws ParseException {
	this(new StringInputStream(in));
    }

日本語を通す

JavaCCは最初のバージョンから日本語が通りませんでした。途中でユニコードの入力もOKになったはずですが、バグがあってしかも未だに直っていません。日本語を通す方法として、以下の方法があります。

読ませたいファイルをnative2asciiなどでASCII文字列に変換する。
上の「JavaCCで日本語を扱う方法」にあります。 しかし、毎回変換するのは大変です。
JavaCCの出力ソースにパッチを当てる。
これも上の「JavaCCで日本語を扱う方法」にあります。 将来のバージョンに渡ってパッチが当たるか不安です。
自分で入力系を作る。
速度的に遅く、入力を変換してしまうので入力文書が保存されてないと 困る場合は使えません。以下説明します。

StringInputStreamに改良を施す

以下のように、文字コードが128以上の文字は、ユニコードエスケープにしてしまいます。これを上の「StringInputStream」クラスに適当に加えて変換を行わせます。

    public static String escape(String in) {
	StringBuffer buf = new StringBuffer();
	for (int i=0;i<in.length();i++) {
            int code = (int)in.charAt(i);
	    
	    if (code >= 128) {
		buf.append("\\u"+Integer.toHexString(code));
	    } else {
                buf.append(in.charAt(i));
            }
	}
	return buf.toString();
    }

JavaCCソース

以下の2つのスイッチを設定します。

    UNICODE_INPUT=false;
    JAVA_UNICODE_ESCAPE=true;

これで、日本語は自動的にASCIIコードによるエスケープに置き換わって入力されますので、日本語を含んだ文章を解釈できます。動作は遅いかもしれませんが、自前の解釈プログラムを作るよりはずっと時間の短縮になります。

なお、Javaのプログラムについて一般的に言えることですが、全角の「〜」などの文字はユニコードとの変換の過程で化ける可能性があります。

まとめてスキップしたい部分がある

SPECIAL_TOKEN を使う方法がありますが、ここではJAVA_CODEを使ってトークンを自分で判断してスキップしてしまうケースについて説明します。TeX の数式でのテキスト部分の解釈の例を挙げます。

「\text{This is a pen.}」を解釈します。ここでは「| \text | { | This is a pen. | } 」と解釈したいとします。「\text」を見つけた時点で次のメソッドを呼びます。

JAVACODE 
String contents() 
{
    Token curTok = token;
    Token tok = getNextToken();  // 次のトークンを読む
    if (!(tok.kind == LBRACE)) { // LBRACE = "{"
	token = curTok;   // 左カッコではなかったら
	return null;      // トークンを一つ前に戻して戻る
    }
    int nesting = 1;
    StringBuffer content = new StringBuffer();
    while (true) {
        tok = getNextToken();
	if (tok == null) break;
	if (tok.kind == RBRACE) { // RBRACE = "}"
	    nesting--;
	    if (nesting == 0) {
		break;
	    }
	}
	if (tok.kind == LBRACE) {
	    nesting++;
	}
	content.append( tok.image ).append(' ');
                // トークンの切れ目に空白を入れる
                // (ここは各適用場面によって考える必要がある)
    }
    return content.toString().trim();
}

帰り値として、「{ }」で囲まれた部分が文字列として帰ってきます。複数回入れ子になっていても大丈夫です。(これが正しいJAVA_CODEの使い方か良く分かっていませんが・・・)

return


桜井雅史: E-mail : m.sakurai@cmt.phys.kyushu-u.ac.jp
Web page : http://www.cmt.phys.kyushu-u.ac.jp/~M.Sakurai/
Last modified: Fri Jan 09 12:05:49 2004