JFlex とは?
今回は、Lucene というオープンソースの全文検索ライブラリを読もうと思ったら、意味不明な Javaファイルがあって、調べたら JFlex で書かれていたことが判明したのですが、その JFlex のファイルに関する 情報が無かったので記事にしました。どこかで同じように、JFlex の謎ファイルを見つけた人の一助になればと思います。
JFlex とは、パーサーで言うところの lexer を作ってくれるライブラリで、作った lexer で文字列をブロックに分けたりなどのことを自分でカスタマイズできます。
使い方は、flex ファイルという特別なファイルを作って、それを JFlex の jar にかませると、lexer が実装された Java ファイルが出力されるようになっている。
噛み砕いて説明すると、
1:JFlex の jar をダウンロード
2:flex ファイルを作成する。(今回は作ってあるものを拝借しました。)ここから、下のコマンドを叩けば、Java ファイルが出力されます。
java -jar jflex-full-1.8.2.jar -d output ./calc.flex
※ 今回の calc.flex は、同じディレクトリ にある Parser.java などを使わないと動きません。
今回は、この java -jar jflex-full-1.8.2.jar
以下のコマンドを叩くと吐き出される Yylex.java の中身を見ていきたいと思います。
また、今回見ていくファイルは、少し古いバージョンの JFlex がはいていた このファイル になります。時間があったら、最新バージョンのものも見て、差分をまとめたいと思います。
JFlex ファイルの中身
Yylex.java のファイルを見るにあたって、ファイルの主な機能は、
1:Unicode の対応表などの変数を作る機能 (ZZ_CMAP, ZZ_ROWMAP, ZZ_TRANS, ZZ_ATTRIBUTE, ZZ_ACTION)
2:ファイルの読み込みの機能 (zzRefill など)
3:lexer の機能 (yylex)
になります。
ここでは、ZZ_CMAP, ZZ_TRANS, ZZ_ROWMAP, ZZ_ATTRIBUTE, ZZ_ACTION の 5つの変数の説明をします。
ZZ_CMAP
private static final char [] ZZ_CMAP = {
0, 0, 0, 0, 0, 0, 0, 0, 7, 6, 3, 0, 0, 4, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
6, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 0, 5, 2, 5,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
ここでは、ZZ_CMAP という Unicode と それに対応するラベルを 決めています。
flex ファイルでは、
NUM = [0-9]+ ("." [0-9]+)?
NL = \n | \r | \r\n
"+" |
"-" |
"*" |
"/" |
"^" |
"(" |
")" { return (int) yycharat(0); }
\b { System.err.println("Sorry, backspace doesn't work"); }
/* error fallback */
[^] { System.err.println("Error: unexpected character '"+yytext()+"'"); return -1; }
のように、中で使う文字列を定義しましたが、その中身が 1つ1つ ラベル付けされています。
上の例で見ると、正規表現で使われている 0〜9 の数字は 1でラベル付けされ(Unicode の 48〜58番目)、正規表現で使われている「.」は 2でラベル付けされていて(Unicode の 46番目)・・・のような感じで、今回は 7 の back space まで定義されています。
簡単にまとめると、下のような変数定義になります。
- 1 → 数字
- 2 → .
- 3 → 改行
- 4 → < return >
- 5 → / - + * ( )
- 6 → スペース
- 7 → バックスペース
ZZ_TRANS
private static final String ZZ_TRANS_PACKED_0 =
"\1\2\1\3\1\2\1\4\1\5\1\6\1\7\1\10"+
"\11\0\1\3\1\11\10\0\1\4\12\0\1\7\2\0"+
"\1\12\6\0";
/*
1 2 1 3 4 5 6 7
-1 -1 -1 -1 -1 -1 -1 -1
-1 2 8 -1 -1 -1 -1 -1
-1 -1 -1 3 -1 -1 -1 -1
-1 -1 -1 -1 -1 -1 6 -1
-1 9 -1 -1 -1 -1 -1 -1
*/
元の変数と、それを関数に入れた後の結果を表示しています。
後の説明で触れますが、DFA (決定性有限オートマトン) の対応表になっていて、この表をもとに lexer する際に 前回の状態を反映します。
ZZ_ROWMAP
private static final String ZZ_ROWMAP_PACKED_0 =
"\0\0\0\10\0\20\0\10\0\30\0\10\0\40\0\10"+
"\0\50\0\50";
// 0 8 16 8 24 8 32 8 40 40
上の ZZ_TRANS は今の状態(lexしている文字)と前の状態を反映した今の状態がの2つの状態を示しています。0〜7で示されるのが現在進行形で lex している 状態で、今回の ZZ_ROWMAP では 前の何の状態を反映したかを 8の倍数で示しています。
例えば、最初の状態(ZZ_ROWMAPは0から始まる)で ZZ_CMAP でいうところの 1 (数字) が入力されると、まずは ROWMAP (前回の状態) + ZZCMAP(現在 lexしている文字) の対応表で ZZ_TRANS[1] つまり 2が出力されます。
この2が前回の状態として、次の lex をする際に、ROWMAP に入力されます。なので次も ZZ_CMAP の 1 (数字) が入力されると、次には ROWMAP = 16 + ZZ_CMAP = 1 の対応表で ZZ_TRANS[17] つまり 2が出力されます。
ここで例えば 次に 「.」 つまり 2 が入力されると、ROWMAP = 16 + ZZ_CMAP = 2 の対応表で ZZ_TRANS[18] つまり 8 が出力されます。
この 8と対応する ZZ_ROWMAP は 40 になりますが、ZZ_TRANS の 40以降は 1つ目が 9 で それ以外は -1(無効)になるので、数字 → 「.」 の後は 数字しか受け付けないことが分かります。
上のコードは、コードの yylex の部分を説明したものになります。
ZZ_ATTRIBUTE
private static final String ZZ_ATTRIBUTE_PACKED_0 =
"\1\0\1\11\1\1\1\11\1\1\1\11\1\1\1\11"+
"\1\0\1\1";
// 0 9 1 9 1 9 1 9 0 1
元の変数と、それを関数に入れた後の結果を表示しています。
これは、上の ZZ_TRANS の結果を判別するための対応表になっています。
基本的には、9 (ZZ_ROWMAPが0の場合は、ZZ_CMAP で言う 0 / 2 / / 3 / 5 / 7 => 文字列など | . | 改行 | "/ - + * ( )" | バックスペース ) であれば 結果の判定をするようになっています。結果の判定に飛ぶのは、他には EOF にあたった時です。
ZZ_ACTION
private static final String ZZ_ACTION_PACKED_0 =
"\1\0\1\1\1\2\2\3\1\4\1\5\1\6\1\0"+
"\1\2";
// 0 1 2 3 3 4 5 6 0 2
最後に、結果の判定をするための対応表になっています。
ファイルの読み込みである zzReFill は比較的簡単な実装になっているので飛ばします。
yylex の説明は、上の DFA が分かれば、このくらい軽量のファイルであれば分かると思います。実際にコードを読んで 内容の理解を深めてもらえればと思います。
再度になりますが、謎の Javaファイル の ZZ_CMAP やら ZZ_TRANS が分からない人のためになればと思います。最初は、自分も意味が分からなかった。