リエントラントな字句/構文解析機を作る

今更だけれど、あんまりきちんとまとまったものがなかったので。クラスを使った場合は、今までとは結構書き方が変わるのとautotoolsとの相性が非常によくない(ylwrapを書き換えればうまくいくと思う)ので、字句解析側、構文解析側ともに、従来の書き方で。

まずはflex側。

再入可能な字句解析機に変更する

%option reentrant

を指定すると、再入可能な字句解析機が出来る。c++のlexerを作るよりも、既存のコードを書き換える量が少なくてすむかも。
これで作成されるlexerは

extern int yylex (yyscan_t yyscanner);

となる。yyscan_t は実際は void* で、字句解析時には struct yyguts_t* にキャストされて利用される。構文解析側からは、yyscan_t を同様に定義するか邪魔くさければ void* でも良いかも。

struct yyguts_t には、従来グローバルであった yyin, yyout, YY_BUFFER_STATE などが入っており、字句解析時にはこちらが利用される。ユーザー側からは従来の名前でアクセスできるようにマクロが用意されており、字句解析側でアクセスしていた箇所は特に変更の必要はない。構文解析側からのアクセスには、アクセッサが用意されており従来のメンバにアクセス可能。

ただし、yylvalについては、構文解析側も再入可能にするために、字句解析側から利用できなくなる。これは yyguts_t に用意されているextraエリアを用いてアクセスする。

{string}	{
	YYSTYPE& yylval = *((YYSTYPE*)yyextra);
	yylval.strval = strdup(yytext);
	return STRINT;
}

flex側の再入可能に変更する部分は、これくらい。yylvalにアクセスしている場所だけ修正すれば、ほとんど修正らしい修正をせずにも、再入可能な字句解析機が出来る。

次にbison側。

再入可能な構文解析機に変更する

%pure_parser

ひとまずは、これで再入可能なparserが作成される。具体的にはyylvalが局所変数になり、yylexの呼び出し形式が

int yylex(YYSTYPE* yylvalp); 

となる。ただし、これではさっきの字句解析側の定義と異なるので、そのままではまだ利用できない。そこで、さらにオプションを追加する。

%parse-param	{ struct yy_parse_state* parse_state }
%lex-param	{ struct yy_parse_state* parse_state }

これで、yylex/yyparse に 引数が追加され、呼び出し、および yyerror が

int yylex (YYSTYPE* yylvalp, struct yy_parse_state* parse_state);
int yyparse (struct yy_parse_state* parse_state);
void yyerror (struct yy_parse_state* parse_state, const char* str);

となる。struct yy_parse_state には yyscan_t と、後は必要なメンバを定義すれば良い。パラメータは、構造体にする必要は無いが、今回はエラー出力時にファイル名が欲しかったので、以下のように定義した。

struct yy_parse_state
{
    void* yyscanner;
    char* yyfile;
    int yynerrs;
};

前準備はこれぐらい、後は、必要な実装を行う。まずは、呼び出し部分。

int parse(const char* file)
{
    FILE* in = fopen(file, "r");
    if (NULL == in) return -1;

    yy_parse_state parse_state;
    parse_state.yyfile = file;
    parse_state.yynerrs = 0;
    yylex_init(&parse_state.yyscanner);  // (1)
    yyset_in(in, parse_state.yyscanner); // (2)
    yyparse(&parse_state);               // (3)

    return parse_state.yynerrs;
}

必要な処理としては、

  1. lexerが利用する yyguts_t の確保と初期化を行ってもらう。これで、lexer側で、局所変数を利用できるようになる。
  2. アクセッサを利用して、入力ファイルの設定を行う。出力ファイルを変更する場合や、以前グローバルであった字句解析側の変数にアクセスする場合も同様の処理で行う。
  3. パーサーを呼び出す。%parse-param で定義した名前でパターンマッチ時に変数を参照することが出来る。

次に、lexerの呼び出しを用意する。

int yylex (YYSTYPE* yylvalp, struct yy_parse_state* parse_state)
{
    yyset_extra(yylvalp, parse_state->yyscanner); // (1)
    return yylex(parse_state->yyscanner);         // (2)
}
  1. yyguts_t のextra領域に yyparse で用意された局所変数のポインタをセットする。
  2. %option reentrant で定義した字句解析機には yyscan_t 型の変数が必要になる。yylex_init で、確保/初期化を行ってもらった場所を利用する。

これで、yylexの呼び出し部分もリエントラントを指定した場合の方法で呼び出すことが出来るようになった。

最後に、yyerrorも呼び出しが変更されているので以下のように変更する。

void yyerror(struct yy_parse_state* parse_state, const char* str)
{
    const char* file = parse_state->yyfile;
    int line = yyget_lineno(parse_state->yyscanner); // (1)
    printf(stderr, "%s:%d:%s\n", file, line, str);
    ++parse_state->yynerrs;
}
  1. 従来のグローバル変数ではなく、構文解析機側に用意されているアクセッサ経由で行番号を取得する。

これで、一通り再入可能な字句解析の部分は終わり。最後にもしも構文解析時に従来のグローバル変数にアクセスしているのであれば、その部分も変更する。簡単な例

line : EOL      { exit (0); }
     | expr EOL	{ FILE* out = yyget_out(parse_state->yyscanner); fprintf(out, "%g\n", $1); } /* (1) */
     ;
  1. 解析句は、yyparse 実行時のジャンプ処理になっているので、yyparse 処理時の変数名でアクセス可能。

と、だいたいこんな感じ。あと必要な yyget_xxx や、yyset_xxx を extern すれば良いかな。

終わりに

相変わらず bison さんと flex さんは、あんまり仲が良くないなあと。
あと、今時 lexer/parse かよとか。結構便利ですよ。
とりあえず当初の目的、複数の入力ファイルから入力したい。でもって、字句解析中に構文解析するのもなあ(スタート状態を使えばうまく処理は出来るものの)というのは無事達成して実行ファイルはなんとか動いていますが、別スレッドとかでは試してないので服用は自己責任で。