古いネタをひっぱりだすシリーズその1

http://kentn.at.infoseek.co.jp/cpp/parser.htmlから移動。
この当時はまだSpiritを知らなかった頃です。たぶん2年かそこら前じゃないかと。
今はどうなってるんでしょうね?

はじめに

ここではメジャーなパーサジェネレータflex & bisonをC++から使う方法を考えてみます。この二つのジェネレータはどちらももともとC言語用に開発されましたが、再入可能性などはあまり深く考えられていませんでした。また、C++から利用する場合にはいくつか不便なところがありました。

ここではよりC++と親和性の高いflex & bisonの使い方を考えます。最終的にはヘッダーファイル一つといくつかの手順を守ることで、手軽にパーサクラスを作成できるようになりました。
とりあえずflexとbisonについて

flexとbisonはどちらもGNUの開発しているユーティリティで、それぞれレキサ(トークナイザ:字句解析器)とパーサ(構文解析器)を生成するジェネレータです。

これらは元々AT&Tで開発されたlexとyaccを元にしており、それぞれの仕様に従った文法で定義を書いていけば、最終的にC言語のプログラムをはき出してくれます。

たとえば、以下はflexで数値と文字列を切り出すプログラムです。

%{
#include 
%}
%%
[0-9][0-9]* { printf("number:%d\n", atoi(yytext)); }
[a-zA-Z][0-9a-zA-Z]* { printf("string:%s\n", yytext); }
%%
int main()
{
  yylex();
  return 0;
}

このように、正規表現を使ってトークン(字句)を定義すれば、それが現れた時に該当する処理を起動させることができます。このファイルを"hoge.l"として保存すると、

flex hoge.l
cc lex.yy.c

のようにしてコンパイルできます。

同様に、簡単なbisonのプログラムを示すと、

%{
#include 
%}
%union {
  double number_;
}
%token  NUMBER
%type  expr, additive_expr, multiplicative_expr, primary
%%
answer
  : expr
  { printf("answer=%f\n", $1); }
  ;
expr
  : additive_expr
  ;
additive_expr
  : multiplicative_expr
  | additive_expr '+' multiplicative_expr
  { $$ = $1 + $3; }
  | additive_expr '-' multiplicative_expr
  { $$ = $1 - $3; }
  ;
multiplicative_expr
  : primary
  | multiplicative_expr '*' primary
  { $$ = $1 * $3; }
  | multiplicative_expr '/' primary
  { $$ = $1 / $3; }
  ;
primary
  : NUMBER
  | '(' expr ')'
  { $$ = $2; }
  ;
%%
int yywrap() { return 0; }
int main() { yyparse(); return 0; }

これは四則演算をするbisonプログラムです。末尾に見慣れたmain関数がありますが、その中で読んでいるyyparse関数というものこそ、bisonが生成する構文解析器の本体です。

flexC++

さて上で見たように、flexとbisonの基本動作は、指定されたタイミングで与えられたC言語のコード断片を実行するものです。この断片をC++で記述することもできます。やってみましょう。

%{
#include 
%}
%%
[0-9][0-9]* { std::cout << "number:" <%lt; atoi(yytext) << std::endl; }
[a-zA-Z][0-9a-zA-Z]* { std::cout << "string:" <%lt; yytext << std::endl; }
%%
int main()
{
  yylex();
  return 0;
}
flex hoge.l
c++ lex.yy.c

これはこれで良いのですが、実はflexにはC++サポートが備わっています(ずーっと『実験的なもの』とされ続けていますが)ので、これを利用してみます。

やり方はコマンドラインで引数-+を与えるか、%option c++を追加するだけです。ここでは後者を用いています。

ちなみに環境によってはflex++というコマンドが用意されている場合もあります。これはflexを-+オプションつきで呼び出したのと同じ動きをします。

%{
#include 
%}
%option c++
%%
[0-9][0-9]* { std::cout << "number:" <%lt; atoi(yytext) << std::endl; }
[a-zA-Z][0-9a-zA-Z]* { std::cout << "string:" <%lt; yytext << std::endl; }
%%
int main()
{
  FlexLexer* lexer = new yyFlexLexer();
  lexer->parse();
  return 0;
}

呼び出し側のコードを多少書き換える必要がありますが、まあほとんど変えていません。

flexC++対応で何がうれしいかというと、iostreamを使えるようになる点が上げられます。

従来のflexでは、データの入出力はC言語のファイル操作関数を用いていました。yyin/yyoutというFILE*型の広域変数が用意されており、これに任意のファイルハンドルを代入させることで、入力を切り替える事ができました。

int main()
{
  FILE* fp = fopen("hoge.input", "rb");
  yyin = fp;
  yylex();
  close(fp);
  return 0;
}

…が、C++版では以下のようになります。

int main()
{
  FlexLexer* lexer = new yyFlexLexer();
  std::ifstream in("hoge.input" std::ios::binary);
  lexer->switch_stream(&in);  
  lexer->parse();
  return 0;
}

iostreamでさえあれば、その先が何であっても同様に渡すことができます。std::fstream/std::stringstreamはもちろん、自作のストリームも渡すことができます。

bisonとC++

flexC++をサポートしたとなれば、そのflexを利用するbisonもC++サポートがついて当然。flexflex++なら、bisonはbison++だ!…と考えるのは自然です。

ところが、どうも話はそう簡単ではないのです。

まず、bisonに埋め込むコード断片は、C++で記述することができます。これはflexの時と同様です。しかし、bisonにはflexでいう-+オプションや%option c++といった命令は用意されていません。

最も痛いのは、bisonがflexが出力したC++コードを標準でサポートしていない点です。これは、ラッパ関数を用意することで切り抜けられます。

inline int yylex() {
	return yyFlexLexer().yylex();
}

が、試してみると分かりますが、これでは上手く行きません。なぜなら、yyFlexLexerのオブジェクトをyylex関数の呼び出しごとに生成するため、yyFlexLexerが内部で持っている先読み情報が失われてしまうからです。正しくスキャンするためには、一つのyyFlexLexerインスタンスを使い回す必要があります。

たとえば広域変数を一つ用いることで、上手く回避できます。

%{
#include 
#include 

FlexLexer* lexer = NULL;
inline int yylex() { return ((FlexLexer*)lexer)->yylex(); }
%}
%union {
  double number_;
}
%token  NUMBER
%type  expr, additive_expr, multiplicative_expr, primary
%%
answer
  : expr
  { printf("answer=%f\n", $1); }
  ;
expr
  : additive_expr
  ;
additive_expr
  : multiplicative_expr
  | additive_expr '+' multiplicative_expr
  { $$ = $1 + $3; }
  | additive_expr '-' multiplicative_expr
  { $$ = $1 - $3; }
  ;
multiplicative_expr
  : primary
  | multiplicative_expr '*' primary
  { $$ = $1 * $3; }
  | multiplicative_expr '/' primary
  { $$ = $1 / $3; }
  ;
primary
  : NUMBER
  | '(' expr ')'
  { $$ = $2; }
  ;
%%
int yywrap() { return 0; }
int main() { lexer = new yyFlexLexer(); yyparse(); return 0; }

再入可能なパーサとFlexLexerの拡張

><

(予定)

BisonParser.h

ということで、こういうヘッダを用意してみました。これをflexとbisonの両方のソースファイルからインクルードすると、Parserクラスができあがります。

最終的には以下のように使えるものになりました:

%{
#include 
#include 
%}
%option c++
%%
[0-9][0-9]* { std::cout << "number:" <%lt; atoi(yytext) << std::endl; return NUMBER; }
[a-zA-Z][0-9a-zA-Z]* { std::cout << "string:" <%lt; yytext << std::endl; }
%%


%{
#include 
#include 
%}
%union {
  double number_;
}
%token  NUMBER
%type  expr, additive_expr, multiplicative_expr, primary
%%
answer
  : expr
  { printf("answer=%f\n", $1); }
  ;
expr
  : additive_expr
  ;
additive_expr
  : multiplicative_expr
  | additive_expr '+' multiplicative_expr
  { $$ = $1 + $3; }
  | additive_expr '-' multiplicative_expr
  { $$ = $1 - $3; }
  ;
multiplicative_expr
  : primary
  | multiplicative_expr '*' primary
  { $$ = $1 * $3; }
  | multiplicative_expr '/' primary
  { $$ = $1 / $3; }
  ;
primary
  : NUMBER
  | '(' expr ')'
  { $$ = $2; }
  ;
%%
int yywrap() { return 0; }
int main() { Parser parser; parser.parse(); return 0; }

以下がその実装です。

/*
 * BisonParser.h --- a C++ parser class for bison & flex++
 *
 *    Copyright (C) Kent.N, 2002  All rights reserved.
 *
 *  Feel FREE to use/distribute/modify this codes. Enjoy!
 *
 */

#if defined(__cplusplus)

#if !defined(__PARSER_DECLARED__)
#define __PARSER_DECLARED__

class MyLexer;

class Parser {
public:
	MyLexer* lexer_;
	int parse();
};

#endif /* __PARSER_DECLARED__ */

#if defined(YYBISON)

#define YYPARSE_PARAM parser
#define YYLEX_PARAM parser

extern "C" int yylex(YYSTYPE* yylval, void* parser);

#endif /* YYBISON */

#if defined(FLEX_SCANNER)

class MyLexer : public yyFlexLexer {
public:
	YYSTYPE yylval;
	int yylex();
};

inline int yyFlexLexer::yylex() { return 0; }
#define YY_DECL int MyLexer::yylex()

extern int yyparse(void*);

int Parser::parse() {
	lexer_ = new MyLexer();
	return yyparse(this);
}

extern "C" int yylex(YYSTYPE* yylval, void* parser) {
	MyLexer* lexer = static_cast(parser)->lexer_;
	int r = lexer->yylex();
	*yylval = lexer->yylval;
	return r;
}

#endif /* FLEX_SCANNER */

#endif /* __cplusplus */

オチ

…ただし、これはバグがあって上手く動かなかった気がする。
何せ昔のことなのでよく覚えてないけれど…(^^;