JFlex と jacc を同時に使う


JFlex と jacc を併用するとき、JFlex で生成される yylex メソッド は、トークン (終端記号) の情報を戻り値とし、それを Jacc が利用します。 トークンが 1 文字の場合は、通常、戻り値は文字コードそのものです。トーク ンが 2 文字以上の場合は、Jacc で %token により宣言されたマクロ を使用します。

ここでは 2文字以上の演算子の例として、*+ という演算子を x *+ yx * 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}+)?

%%
  \r|\n|\r\n                     { return EOL; }
  {ws}+                          { /* 無視する */ }
  {number}                       { yylval = Double.parseDouble(yytext()); return NUMBER; }
  [+\-*/()=]                     { return (int)(yytext().charAt(0)); }
  "*+"                           { return FOO; }
/* 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()'('NUMBERFOO NUMBER')''*'NUMBER'\n'
yylval 12.0  34.0  56.0 

jacc のソースファイル (Calc.jacc)の方は、 生成するクラスのコンストラクタを定義し、 lexer というフィールドに JFlex が生成するクラスのインスタンス が代入されるようにしておきます。

%next, %get, %semanticなどのオプションは、 JFlex の方で jacc のデフォルトの名前のメソッドを用意するので指定する必要はありません。

生成規則の中で、トークン(終端記号)として文字リテラル ('+', '-'など) と %token で宣言したマクロを用いることができます。


// 属性値の型
%semantic double
// 生成されるクラスの名前
%class CalcParser
// 生成されるインタフェースの名前
%interface CalcTokens

%token NUMBER EOL  /* jacc cannot handle '\n' properly */
%token FOO
  /* 演算子の優先順位 */
%left  '+' '-' FOO
%left  '*' '/'

%%
   /* 文法記述とそれに対する動作(還元時に実行されるプログラム) */
   /* 通常の BNF の → の代わりに : を書く。各 BNF は ; で区切る。 */
   /* 最初に start symbol を書く */
input :   /* 空 */
      | input line EOL    {}
      ;

   /* $$ は左辺の属性値、$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から、 %class, %interface ディレクティブで名前を指定した Java ソースファイル(%class, %interface ディレクティブがない場合は、jacc ソースファイルの名前に Parser, Tokens を後づけした名前になる。Calc.jacc の場合 CalcParser.java, CalcTokens.java になる)も生成されます。

あとはこの 3 つの Java ソースファイルをまとめてコンパイルします。 CalcParser.java からあとの二つのクラスを利用しているので、 次のコマンドで一度にコンパイルできます。

  javac CalcParser.java

これでいくつかの classファイルが生成されます。次のコマンドで実行できます。

  java CalcParser

コンパイラのホームページ
Koji Kagawa