ここでは 2文字以上の演算子の例として、*+ という演算子を x *+ y が x * 256 + y を表し、+, -と同じ優先順位を持つ、 左結合の演算子として導入します。仮に FOO 演算子と呼ぶことにします.
JFlex のソースファイル(calc.jflex)には次のような %implements オプションを入れておきます。
implement されるクラス (CalcTokens) は jacc が生成するクラスで、 %token により宣言されたマクロが定義されています。
また、jacc はデフォルトで lexer というフィールドの nextToken, getToken, getSemantic という メソッドを利用するので、これらのメソッドを定義しておきます。
/* import 宣言はここに書く */
import java.io.IOException;
%%
// 生成するクラスの名前
%class CalcLexer
// 生成するクラスが実装するインタフェース
%implements CalcTokens
// 生成するメソッドの戻り値型
%int
%unicode
%line
%column
%{
/* jacc とのインタフェースのためのコード */
int token;
double yylval;
int nextToken() {
try {
return token = yylex();
} catch (IOException e) {
return token = ENDINPUT;
}
}
int getToken() {
return token;
}
double getSemantic() {
return yylval;
}
%}
ws = [ \t\f]
digit = [0-9]
number = {digit}+(\.{digit}+)?(E[+\-]?{digit}+)?
%%
{ws}+ { /* 無視する */ }
{number} { yylval = Double.parseDouble(yytext()); return NUMBER; }
[+\-*/()=\n] { return (int)(yytext().charAt(0)); }
"*+" { return FOO; }
\r { /* 無視する */ }
/* error fallback */
. { throw new Error("不正な文字です <"+ yytext()+">"); }
動作の中に return 文を入れておくと、 その式の値が JFlex の生成する yylex メソッドの戻り値になります。 yylex メソッドは呼び出されるたびに、次のトークンの情報を返します。
終端記号(トークン)は、1文字からなるトークンの場合は通常、 文字コードそのまま、2文字以上からなるトークンの場合は %token で宣言されたマクロです。
トークンの “種類”(NUMBER など)を yylex メソッドの値として返し、値(“属性”)を yylval というフィールドに代入していることに注意します。
一般に正規表現にマッチした文字は、 yytext() というメソッドで得ることができます。
例えば、入力が “(12*+34)*56\n” という文字列の場合、 yylex() の戻り値とそのときの yylval の値は次のようになります。
| 1回目 | 2回目 | 3回目 | 4回目 | 5回目 | 6回目 | 7回目 | 8回目 | |
|---|---|---|---|---|---|---|---|---|
| yylex() | '(' | NUMBER | FOO | NUMBER | ')' | '*' | NUMBER | '\n' |
| yylval | 12.0 | 34.0 | 56.0 |
jacc のソースファイル (Calc.jacc)の方は、 生成するクラスのコンストラクタを定義し、 lexer というフィールドに JFlex が生成するクラスのインスタンス が代入されるようにしておきます。
%next, %get, %semanticなどのオプションは、 JFlex の方で jacc のデフォルトの名前のメソッドを用意するので指定する必要はありません。
生成規則の中で、トークン(終端記号)として文字リテラル ('+', '-'など) と %token で宣言したマクロを用いることができます。
// 属性値の型
%semantic double
%token NUMBER
%token FOO
/* 演算子の優先順位 */
%left '+' '-' FOO
%left '*' '/'
%%
/* 文法記述とそれに対する動作(還元時に実行されるプログラム) */
/* 通常の BNF の → の代わりに : を書く。各 BNF は ; で区切る。 */
/* 最初に start symbol を書く */
input : /* 空 */
| input line '\n' {}
;
/* $$ は左辺の属性値、$n は右辺の n 番目の文法要素の属性値を表す。 */
line : /* 空 */ { System.exit(0); }
| expr { System.out.printf("%g%n", $1); }
;
expr : expr '+' expr { $$ = $1 + $3; }
| expr '-' expr { $$ = $1 - $3; }
| expr '*' expr { $$ = $1 * $3; }
| expr '/' expr { $$ = $1 / $3; }
| expr FOO expr { $$ = $1 * 256 + $3; }
| '(' expr ')' { $$ = $2; }
| NUMBER { $$ = $1; }
;
%%
/* フィールドやメソッドの定義 */
CalcParser(CalcLexer l) {
lexer = l;
}
private CalcLexer lexer;
private void yyerror(String msg) {
System.out.println("エラー: " + msg);
System.exit(1);
}
public static void main(String[] args) {
CalcLexer lexer = new CalcLexer(System.in);
CalcParser calc = new CalcParser(lexer);
lexer.nextToken();
calc.parse(); // parse the input
}
Java ソースファイルはそれぞれ次のコマンドで生成します。
java -jar jacc.jar Calc.jacc java -jar JFlex.jar calc.jflex
すると、calc.flex から、CalcLexer.javaが、 Calc.jaccから、 jacc ソースファイルの名前に Parser, Token を後づけした Java ソースファイル(この場合 CalcToken.java, CalcParser.java)も生成されます。
あとはこの 3 つの Java ソースファイルをまとめてコンパイルします。 CalcParser.java からあとの二つのクラスを利用しているので、 次のコマンドで一度にコンパイルできます。
javac CalcParser.java
これでいくつかの classファイルが生成されます。次のコマンドで実行できます。
java CalcParser