この章ではCおよびBisonと一緒にFlexを使う方法を説明します。(7)C、Bisonのそれぞれが非常に多くの細目を含むため、 本章は2つの部分に分割されています。 その両方に、 全般的なインターフェイス概念に関する節と実例を示す節があります。
Flexに対するCの主要なインターフェイスは、 以下に挙げるルーチンと変数によるものです。 以下の節を読む際には、 いくつかの細かな部分でFlexとLexとの間に相違点があるということを意識しておいてください。 Lexが提供していない関数がいくつかありますし、 宣言の内容が違うものもあります。 こうした相違点は、 通常大きな問題にはなりません。 というのは、 相違のある関数は一般的にはあまり使われていないからです。 相違点に関する詳細については、 Flex と Lexおよび FlexとPOSIXを参照してください。
関数
yylex()
yylex()
は実際のスキャン処理を行う関数です。
ファイル(デフォルトはstdin
)を読み込み、
パターン・マッチングを行い、
パターンに関連付けされたアクションを実行します。
デフォルトでは、
入力の終端に達するまでマッチングを行い、
終端に達したところでゼロを返します。
(return
を使って、
呼び出し側のプログラムにほかの値を返すことは可能です。
これは、
FlexとBisonで説明しています。)
したがって、
インターフェイスを提供する簡単な方法の1つは、
オプションのCコード領域の1つに以下のようなコードを追加することです。
#include <stdio.h> int main(argc,argv) int argc; char *argv; { yylex(); }しかしこのような場合には、 Flexライブラリ(`-lfl')もしくはLexライブラリ(`-ll')のいずれかをリンクして、 そこからこれと同じような
main()
関数を取り込むことができます。
この場合は、
スキャナは単にファイルをスキャンして、
ルールに関連付けされたアクションを実行するだけであるという点に注意してください。
yylex()
の使い方としてもう1つよく見られるのが、
マッチされたものが何であるかを示す値を返させることです。
これは、
アクションにreturn
文を追加することで行われます。
return
文を見つけると、
yylex()
は指定された値を返します。
これが、
BisonによるパーサがFlexによるスキャナから情報を獲得する方法です。
ルールの中に、
マッチされたテキストが何を表しているかを示すコードを返すreturn
文があれば、
以下のようなインターフェイスを使うことができます。
#include <stdio.h> int main(argc,argv) int argc; char *argv; { int return_code; while((return_code = yylex()) != 0){ switch(return_code){ case KEYWORD1: /* 何かを行う */ break; case KEYWORD2: /* 何か別のことを行う */ break; ... case KEYWORDn: } } }
yylex
のデフォルトの定義は`int yylex(void)'ですが、
これはYY_DECL
マクロを使うことによって変更することができます。
例を示すと、
以下のコードはyylex()
の名前をgettok()
に、
型をcharに対するポインタ型に変更し、
パラメータbuffer
を受け取るように指定します。
#undef YY_DECL #define YY_DECL char *gettok(char *buffer)注:ANSI対応でないCを使っている場合は以下のように定義しなければなりません。
#define YY_DECL char *gettok(buffer) \ char *buffer;言葉を変えると、 再宣言はターゲットとなるCコンパイラにとって正当な関数宣言でなければなりません。 さらに、 この再宣言は、 ファイルの先頭にあるオプションのCコード領域になければならないという点に注意してください。
yyin
yyin
は、
yylex()
が文字を読み込む元となるファイルです。
デフォルトはstdin
ですが、
fopen()
を使って変更することができます。
yyin
を読み込むデフォルトの方法は、
複数文字から成るブロックを一度に読むというものです。
これは、
YY_INPUT
マクロによって変更できます。
YY_INPUT
マクロは、
ファイルではなく文字列をスキャンするためのスキャナを生成するのに便利です。
YY_INPUT
を定義する方法は以下のとおりです。
YY_INPUT(buffer,result,max_size)ここで、
buffer
は入力バッファ、
result
は読み込まれた文字数がセットされる変数、
max_size
はbuffer
のサイズです。
以下に、
一度に1文字ずつ読み込むという入力方法に変更する再定義の例を示します。
この方法を使うとかなり遅くなるので、
お勧めはできません。
#undef YY_INPUT #define YY_INPUT(buffer,result,max_size) \ {\ buffer[0] = getchar();\ if(buffer[0] == EOF)\ result = YY_NULL;\ else\ result = 1;\ }注:この再宣言は、 ファイルの先頭にあるオプションのCコード領域になければなりません。
yyout
yyout
はスキャナがECHO
の出力を書き込むファイルです。
デフォルトはstdout
ですが、
これもfopen()
を呼び出すことで変更できます。
yytext
yytext
は最後にマッチされた文字列、
つまり最後に認識されたトークンを含む大域変数です。
yytext
の正しい外部定義は、
Lexの場合のcharの配列とは異なり、
charに対するポインタ型である点に注意してください。(8)
つまり、
yytext
は
extern char yytext[];ではなく、常に
extern char *yytext;のように宣言されなければならないということです。 このようになっている理由は性能です。
yytext
が配列であると、
スキャナ中でそれを操作するコードは、
コピー処理をたくさん行う必要があります。
これに対してyytext
がポインタである場合には、
このようなことは必要ありません。
通常は、
yytext
は変更すべきではありません。
yytext
の内容が変更される必要がある場合には、
代わりのバッファが使われるべきです。
(`examples'サブディレクトリのyymore2.lex
ファイルでは、
yytext
を直接操作する技法が実演されています。
ただし、
このようなやり方はお勧めできません。)
yyleng
yyleng
は、
最後に認識されたトークンの長さを保持する大域変数です。
yywrap()
yywrap
は、
yyin
の終端に達した時に呼び出される関数です。
この関数がTRUE
(ゼロ以外)を返すとスキャナは終了し、
FALSE
(ゼロ)を返すと、
yyin
が次の入力ファイルを指すように設定されたものと仮定して、
スキャン処理が続行されます。
現在のところyywrap()
は、
常に1を返すよう定義されているマクロです。
このため、
再定義するには、
まず最初に#undef
で定義解除しなければなりません。
Lexでは、
yywrap()
は関数です。
Flexも将来のある時点で、
これを関数として定義することになるでしょう。(9)
yymore()
yymore()
は、
次に認識されるトークンでyytext
の内容を更新するのではなく、
その時点のyytext
の内容の後ろにそのトークンを追加するようFlexに通知する関数です。
したがって、
以下の例に対して`foobar'という文字の並びを入力として与えると、
stdout
に`foofoobar'という文字の並びが書き込まれます。
%% foo ECHO; yymore(); bar ECHO;これは、 まず
foo
ルールによって`foo'という文字の並びが認識されてECHO
され、
次に`bar'という文字の並びが認識されてyytext
の内容の後ろに追加された後に、
`foobar'という文字の並びがECHO
されるからです。
もう少し現実的な例を取り上げましょう。
以下のコードは複数行の文字列を処理するのにyymore()
を使っています。
/* * yymore.lex: yymore()を有効に使う例 */ %{ #include <memory.h> void yyerror(char *message) { printf("Error: %s\n",message); } %} %x STRING %% \" BEGIN(STRING); <STRING>[^\\\n"]* yymore(); <STRING><<EOF>> { yyerror("EOF in string."); BEGIN(INITIAL); } <STRING>\n { yyerror("Unterminated string."); BEGIN(INITIAL); } <STRING>\\\n yymore(); /* 複数行にわたる * 文字列を処理する */ <STRING>\" { yytext[yyleng-1] = '\0'; printf("string = \"%s\"",yytext); BEGIN(INITIAL); } %%この例では、 エスケープ・シーケンスの変換がまったく行われていないので、 文字列に対してさらに処理が必要である点に注意してください。 この例は、 文字列リテラルの処理において、 エスエープ・シーケンスを処理する、 より役に立つ形式に拡張されます。
yyless(n)
yyless()
は、
yymore()
とほぼ反対のことを行うものです。
この関数は、
最初のn文字以外のすべてを戻します。
戻された文字の並びは、
次のトークンをマッチするのに使われ、
yyleng
とyytext
には、
この変化を反映した値が設定されます。
引数nにゼロを指定してyyless()
を呼び出すと、
全入力データが戻され、
スキャナは
(BEGIN
、
またはそれに類似のものでデフォルトの動作が変更されない限り)
無限ループに入ります。
例えば、
次のコードに`foobar'という文字の並びを入力として与えると、
`foobarbar'という文字の並びが出力されます。
%% foobar ECHO; yyless(3); [a-z]+ ECHO;これは、 `foobar'が認識され
ECHO
された後に、
`bar'が戻されるからです。
となると、
次にマッチするのは
(`[a-z]+'というルールでマッチされる)
`bar'だけで、
これが次にECHO
されることになります。
input()
input()
は、
yyin
から次の文字を取って返す関数です。
これは、
標準的なFlexルール・システムを使ったのではうまく扱えないケースを処理するのによく使われます。
例えば、
ほとんどの言語におけるコメントは、
これを使って処理することができます。
これを使う理由は、
%% "/*".*"*/"が、 ピリオドが改行以外の任意の文字にマッチしてしまうために 複数行にわたるコメントをうまく処理できず、 また、
%% "/*"[.\n]*"*/"は、 文字クラスが任意の文字にマッチしてしまうために、 バッファをオーバーフローさせるか、 さもなくばファイルの内容をすべて読み込んでしまうからです。 (実際には、 排他的スタート状態を使うことで、 こうしたことを非常にエレガントな方法で処理することができます。 実例については、 役に立つコードの抜粋を参照してください。 しかし、 POSIXによりサポートされているにもかかわらず、 ここで必要になるいくつかの機能をLexが提供していないために、 この方法には移植性がありません。) Cのコメントは以下のようにして移植性のある方法で処理することができます。
%% "/*" { int a,b; a = input(); while(a != EOF){ b = input(); if(a == '*' && b == '/'){ break; }else{ a = b; } } if(a == EOF){ error_message("EOF in comment"); } }注:スキャナがC++コンパイラを使ってコンパイルされる場合は、 この関数
input
はyyinput
という名前になります。
これは、
input
という名前が同一名のC++ストリームと衝突するからです。
また、
Flexではinput()
はyytext
の内容を破壊しますが、
Lexではyytext
は変更されずそのまま残ります。
これは将来のリリースで修正される予定です。
unput(c)
unput()
は、
文字c
が次にスキャンされる文字になるように、
文字c
を入力ストリームに置く関数です。
例えば、
%% foo unput('b');は`foo'を`b'で置き換えます。 これは、 `foo'にマッチして`b'を戻し、 この`b'が次にスキャンされる文字になるからです。 デフォルトのルールにより、 `b'は
stdout
に書き込まれます。
1つの文字が次にスキャンされる文字になるということには1つ微妙な点があって、
それは、
文字列を入力ストリームに置きたい場合には、
逆順に行わなければならないということです。
以下に例を示します。
foobar { char *baz = "baz"; int i = strlen(baz)-1; while(i >= 0){ unput(baz[i]); i--; } }これは、 `foobar'がマッチされた時に、 入力ストリームに`baz'を置きます。 以下は、 してはならないことを示す例です。
/* * unput.l : unput()を使って行ってはならない * 処理の例 */ %{ #include <stdio.h> void putback_yytext(void); %} %% foobar putback_yytext(); raboof putback_yytext(); %% void putback_yytext(void) { int i; int l = strlen(yytext); char buffer[YY_BUF_SIZE]; strcpy(buffer,yytext); printf("Got: %s\n",yytext); for(i=0; i<l; i++){ unput(buffer[i]); } }この例に`foobar'を入力として与えると、 まず`foobar'にマッチし、 次に`raboof'にマッチする無限ループに陥ります。 注:
input()
と同様にunput()
もyytext
の内容を破壊します。(10)つまり、
yytext
から文字情報を返したい場合には、
上の例に示されるように、
まずyytext
の内容をコピーしなければならないことを意味しています。
yyterminate()
yyterminate()
はスキャナの実行を終了させ、
その後にyylex()
が0を返します。
この後は、
yyrestart()
(下記参照)が呼び出されない限り、
yylex()
を呼び出してもすぐに復帰してしまいます。
yyrestart(file)
yyrestart()
は、
スキャナの実行を再開するようFlexに通知する関数です。
これは引数を1つだけ、
すなわち、
スキャンの対象となるファイル(通常はyyin
)を取ります。
これは、
EOFを処理するために使うこともできますし、
また、
Flexに割り込みをかけ、
その後に再開始させることができるようにするために使うこともできます。
(Flexスキャナは再入可能ではないので、
このようなことが必要になります。)
YY_NEW_FILE
yyin
が新しいファイルを指すよう変更され、
処理が継続されるべきであるということをFlexに通知するマクロです。(11)
以下に例を示します。
/* * cat.lex: YY_NEW_FILEの実演 */ %{ #include <stdio.h> #define ERRORMESS "Unable to open %s\n" char **names = NULL; int current = 1; %} %% <<EOF>> { current += 1; if(names[current] != NULL){ yyin = fopen(names[current],"r"); if(yyin == NULL){ fprintf(stderr,ERRORMESS, names[current]); yyterminate(); } YY_NEW_FILE; } else { yyterminate(); } } %% int main(int argc, char **argv) { if(argc < 2){ fprintf(stderr,"Usage: cat files....\n"); exit(1); } names = argv; yyin = fopen(names[current],"r"); if(yyin == NULL){ fprintf(stderr,ERRORMESS,names[current]); yyterminate(); } yylex(); }
ECHO
yytext
の内容をyyout
に書き込むマクロです。
REJECT
REJECT
は、
その時点においてマッチしているものを受け入れず、
次に最もよくマッチするものを受け入れるようスキャナに通知するマクロです。
スキャナはマッチするものの中で最長のものを探し、
マッチするものが2つあってその長さが同じ場合は、
記述ファイルにおいて最初に定義されている方を選択します。
つまり、
認識されるテキストの長さは、
同一の長さになることもあり、
または短くなることもあるということを意味しています。
REJECT
を使った後は、
yytext
とyyleng
は新しい値を取ります。
REJECT
に関して知っておくべき重要な点が2つあります。
1つめは、
REJECT
は分岐命令であり、
決して戻ってこないので、
REJECT
の後ろに記述されたアクションは実行されないということです。
2つめは、
REJECT
とファスト・テーブル(fast table/`-F')は一緒に使うことはできないということです。
以下に簡単な例を示します。
/* * reject.lex: REJECTとunput()を悪用する実例 */ %% UNIX { unput('U'); unput('N'); unput('G'); unput('\0'); REJECT; } GNU printf("GNU is Not Unix!\n"); %%この例は、 新式のテキスト代替の技法を示しています。 `UNIX'にマッチするものが見つかると、
unput()
によって`GNU'という文字の並びが戻され、
その時点におけるスキャン・バッファの内容が上書きされます。
次にREJECT
により分岐が行われ、
別のものにマッチするようスキャナに対して通知が行われます。
`GNU'がバッファに書き込まれたので、
これが次にマッチされ、
そのアクションが実行されます。
以下に、
その結果こうなるであろうと思われる例を示します。
UNIX return GNU is Not Unix!実際のところは、 Flexにおいて
REJECT
の用途はほんの少ししかありません。
上記以外では、
重複するパターンや状態の変更に使うことができます。
例を示すと、
以下のようになります。
nday [1-9]|[1-2][0-9]|3[0-1] nmonth [1-9]|1[0-2] nyear [0-9]{1,4} %x DAY MONTH YEAR %% {nday} BEGIN(DAY); REJECT; <DAY>{nday} ... {nmonth} BEGIN(MONTH); REJECT; <MONTH>{nday} ... {nyear} BEGIN(YEAR); REJECT; <YEAR>{nday} ...この例では、 日付の形式は重複しており、 最初に認識された構成要素によって、 どのように日付をパースするかを決定します。 しかし、 この例は少々不自然な感じがします。 というのは、 少し考えれば、
REJECT
を使わずに、
より効率的なスキャナにすることができるからです。
これは、
スタート状態の使用例に示しています。
BEGIN
BEGIN
は、
スキャナをある特定のスタート状態にするためのマクロです。
BEGIN
に続く名前はスタート状態の名前です。
例えば、
%x FLOAT %% floats BEGIN(FLOAT) <FLOAT>some_rule some_action ...は、 `floats'という単語がマッチした時に、 スタート状態を
FLOAT
に設定します
(詳細については、
スタート状態の説明を参照)。
YY_USER_ACTION
YY_USER_ACTION
は、
ルール・セクション中のどのアクションよりも前に実行されるアクションを定義するマクロです。
これは、
以下の例で示すように、
yytext
の内容の小文字から大文字への変換等を行うのに役に立ちます。
/* * user_act.lex: YY_USER_ACTIONを使う * ユーザ・アクションの例 */ %{ #include <ctype.h> void user_action(void); #define YY_USER_ACTION user_action(); %} %% .* ECHO; \n ECHO; %% /* * このユーザ・アクションはすべての文字を * 単に大文字に変換する */ void user_action(void) { int loop; for(loop=0; loop<yyleng; loop++){ if(islower(yytext[loop])){ yytext[loop] = toupper(yytext[loop]); } } }これは、 すべての入力文字を単に大文字に変換して
ECHO
します。
YY_USER_ACTION
のデフォルトの設定では、
何も実行されません。
YY_USER_INIT
YY_USER_INIT
は、
スキャン処理が開始される前に実行されるアクションを定義するマクロです。
基本的には、
main()
関数の中で、
yylex()
を呼び出す文の前に同様のコードを記述するのと同じことです。
以下に簡単な例を示します。
/* * userinit.lex: YY_USER_INITを使う例 */ %{ #define YY_USER_INIT open_input_file() extern FILE *yyin; void open_input_file(void) { char *file_name,buffer[1024]; yyin = NULL; while(yyin == NULL){ printf("Input file: "); file_name = fgets(buffer,1024,stdin); if(file_name){ file_name[strlen(file_name)-1] = '\0'; yyin = fopen(file_name,"r"); if(yyin == NULL){ printf("Unable to open \"%s\"\n", file_name); } } else { printf("stdin\n"); yyin = stdin; break; } } } %} %%これは、 ファイルがオープンされるかEOFが検出されるまで、 入力ファイル名を入力するようユーザに催促します。 EOFが検出された場合は、 入力元はデフォルトで
stdin
になります。
これは以下と同じことです。
/* * この例は、前の例と同じことをYY_USER_INITを * 使わずに行う */ %{ void open_input_file(void) { char *file_name,buffer[1024]; yyin = NULL; while(yyin == NULL){ printf("Input file: "); file_name = fgets(buffer,1024,stdin); if(file_name){ file_name[strlen(file_name)-1] = '\0'; yyin = fopen(file_name,"r"); if(yyin == NULL){ printf("Unable to open \"%s\"\n", file_name); } } else { printf("stdin\n"); yyin = stdin; break; } } } %} %% %% int main(int argc, char *argv[]) { open_input_file(); yylex(); }
YY_BREAK
YY_BREAK
はマクロです。
インターフェイス的な機能というよりも、
むしろ生成されるコードを変更するために使うことができるものです。
スキャナ中において、
すべてのアクションは1つの大きなswitch
文の要素であり、
デフォルトでCのbreak;
文に置き換えられるYY_BREAK
によって区切られます。
ルールのアクション部が多くのreturn
文を含んでいる場合、
コンパイラが`statement not reached'というエラー・メッセージをたくさん出力するかもしれません。
YY_BREAK
を再定義することによって、
この警告メッセージの出力を止めることが可能です。
再定義は、
セミ・コロンを含む正当なCの文でなければなりません。
YY_BREAK
を再定義して空にするのであれば、
アクションの最後は必ずreturn;
かbreak;
になるようにしてください。
Flex 2.5では、 前節(FlexとC)で説明されていない、 以下のマクロもサポートされています。
yy_set_interactive(is_interactive)
yy_set_interactive()
による指定は、
`%option always-interactive'や`%option never-interactive'による指定に優先します。
このマクロは、
バッファからのスキャン処理が始まるよりも前に呼び出されなければなりません。
yy_set_bol(at_bol)
yy_set_bol()
は、
バッファ内のカレントな位置が行の先頭にあるか否かを表すコンテキスト情報を設定します。
引数にゼロ以外の値を渡すと、
バッファ内のカレントな位置は行の先頭である、
というコンテキスト情報がセットされます。
したがって、
次にトークンのマッチ処理が行われる時には、
行頭を表す`^'を含むルールの適用が試みられます。
逆に、
引数にゼロを渡すと、
バッファ内のカレントな位置は行の先頭ではないことになり、
次にトークンのマッチ処理が行われる時には、
行頭を表す`^'を含むルールの適用が試みられなくなります。
YY_AT_BOL()
ある単語が現れた時に、 それを別の単語に置き換える必要の生じることがよくあります。 例えば、 ある名前が現れるたびに、 それをある1つの環境変数の値で置き換えてくれるユーティリティを作りたいとしましょう。 そして、 以下のようなことができるように、 そのユーティリティがフィルタとして動作するようにさせたいとします。
nick% myname < infile | more nick% myname < infile > outfile
以下に、 こうしたことを実現する方法を示すFlexファイルの簡単な例を挙げます。
/* * myname.lex : トークンの置き換えを行うFlexプログラム * のサンプル */ %% %NAME { printf("%s",getenv("LOGNAME")); } %HOST { printf("%s",getenv("HOST")); } %HOSTTYPE { printf("%s",getenv("HOSTTYPE"));} %HOME { printf("%s",getenv("HOME")); } %%
このソース・ファイルは`examples'サブディレクトリにあり、 その名前は`myname.lex'です。 これをビルドするには、 `examples'サブディレクトリに移動して`make myname'を実行するか、 以下を実行します。
flex myname.lex cc lex.yy.c -o myname -lfl
ここで`-lfl'は、
リンカに対してFlexライブラリをリンクするよう通知します。
現在のところ、
Flexライブラリにはデフォルトのmain()
関数だけが含まれています。
将来のバージョンのFlexでは、
他の関数も含まれるようになるでしょう。
Flexライブラリがインストールされていない場合は、
この部分は`-ll'でなければなりません。
いずれの場合でも、 最終的には`myname'という名前の実行ファイルが生成されるはずです。 これは、 以下のような変換処理を実行するフィルタです。
%NAME
%HOST
%HOSTTYPE
%HOME
したがって、 以下のような内容を持つファイル`myname.txt'を作成して、
Hello, my name is %NAME. Actually "%NAME" isn't my real name, it is the alias I use when I'm on %HOST, which is the %HOSTTYPE I use. My HOME directory is %HOME.
以下を実行すると、
myname < myname.txt
以下のテキストに似たものがstdout
へ書き込まれます。
Hello, my name is foobar. Actually "foobar" isn't my real name, it is the alias I use when I'm on baz, which is the cray I use. My HOME directory is /home/foo/foobar.
このプログラムがうまく動作するのは、
yyin
とyyout
がデフォルトではstdin
、stdout
にそれぞれ割り当てられ、
かつ、
デフォルトのアクションがyyin
の内容をyyout
にコピーするからです。
また、
個々のルールに対応する唯一のアクションが単一行で記述されているため、
`{ }'は必要ではないことに注意してください。
このような場合には、
アクションを`{ }'で囲むか否かは、
個人的な好みの問題になります。
これが、 引用符で囲まれた部分にあるものも含めて、 指定された名前が現れるところすべてにマッチしたことに気がつきましたか? Flexにおいては、 引用符で囲まれた部分にあるものにマッチさせたくない場合には、 それに対応するルールを作成することにより、 そうしないよう明示的にFlexに通知しなければなりません。 以下に例を示します。
/* * myname2.lex : トークンの置き換えを行うFlexプログラムの例 */ %{ #include <stdio.h> %} %x STRING %% \" ECHO; BEGIN(STRING); <STRING>[^\"\n]* ECHO; <STRING>\" ECHO; BEGIN(INITIAL); %NAME { printf("%s",getenv("LOGNAME")); } %HOST { printf("%s",getenv("HOST")); } %HOSTTYPE { printf("%s",getenv("HOSTTYPE"));} %HOME { printf("%s",getenv("HOME")); }
この例では、 排他的スタート状態を使って、 文字列中のテキストが変更されることのないようにしています。 この例も`examples'サブディレクトリにあるもので、 その名前は`myname2.lex'です。
Bisonは、 Flexと同様、 ある記述情報を受け取って、 それをもとにCのコードを生成するプログラムです。 両者の違いは、 BisonがCやPascalのような言語の文法に関する記述情報を入力として受け取り、 その記述情報からパーサを生成する点にあります。 FlexとBisonを結合することにより、 言語の字句解析と構文解析の両方を処理することができるようになります。 (これは、 コンパイラ・デザインにおいて最も容易に自動化できる部分です。)
生成されるパーサが機能するためには、
Bisonはyylex()
という関数を必要とします。
この関数はユーザによって提供され、
呼び出された時に、
パースされている言語のある要素を表す整数値をBisonに返します。
Flexにおいてスキャン処理を行うルーチンはyylex()
であり、
デフォルトでは整数値を返します。
これにより、
FlexとBisonを一緒に使うのは非常に簡単になります。
警告: 以下の節では、 読者がBisonの基本的なパーサの宣言を理解しているものと仮定します。 Bisonを使った経験のない人には、 パーサの定義は混乱をもたらす可能性がありますので、 先に進む前に是非Bisonのマニュアルを読んでください。 Bisonに興味の無い人は、 この節全体を飛ばしても構いません。
FlexとBisonの間で情報を渡す基本的な方法は、
関数`yylex()'を使うことです。
これは、
Flexにより生成されるスキャナにおいて、
スキャン処理を実行する関数の名前です。
Flexの入力ファイルのアクション部分において`return'文を使うことによって、
単なる0や1以外の値を返すことができます。
この方法で、
yylex()
は最後に認識されたトークンを表す整数値を返すことができます。
Bisonを`-d'オプション付きで使うと、 Bisonは`.tab.h'という拡張子を持つファイルを生成します。 このファイルには、 記述情報中にある正当なトークンの1つ1つに対する一意な定義情報が含まれます。 この出力情報は、 特にスキャナによって使用されることを想定して設計されています。 このファイルをFlexにより生成されたスキャナに含めることで、 2つのプログラムの間に非常に明確なインターフェイスを作ることができます。 例として、 以下にBisonのファイルを示します。 このファイルの名前を`expr.y'としましょう。
/* * expr.y : Bisonマニュアル中の例に基づく * Bisonによる簡単な表現式パーサ */ %{ #include <stdio.h> #include <math.h> %} %union { float val; } %token NUMBER %token PLUS MINUS MULT DIV EXPON %token EOL %token LB RB %left MINUS PLUS %left MULT DIV %right EXPON %type <val> exp NUMBER %% input : | input line ; line : EOL | exp EOL { printf("%g\n",$1);} exp : NUMBER { $$ = $1; } | exp PLUS exp { $$ = $1 + $3; } | exp MINUS exp { $$ = $1 - $3; } | exp MULT exp { $$ = $1 * $3; } | exp DIV exp { $$ = $1 / $3; } | MINUS exp %prec MINUS { $$ = -$2; } | exp EXPON exp { $$ = pow($1,$3);} | LB exp RB { $$ = $2; } ; %% void yyerror(char *s) { printf("%s\n",s); } int main() { yyparse(); }
これは非常に簡単な計算機の文法定義です。
`-y -d'オプション付きで呼び出されると、 Bisonは`y.tab.h'というファイルを生成します。 このファイルには以下のような定義か、 それにきわめてよく似た定義が含まれます。
typedef union { float val; } YYSTYPE; extern YYSTYPE yylval; #define NUMBER 258 #define PLUS 259 #define MINUS 260 #define MULT 261 #define DIV 262 #define EXPON 263 #define EOL 264 #define LB 265 #define RB 266
Flexがトークンの値を正しくBisonに返すことができるように、
(#include
を使って)
これをスキャナに含めることができます。
そのコードは以下のようなものになります。
/* * expr.lex : 簡単な表現式パーサのためのスキャナ */ %{ #include "y.tab.h" %} %% [0-9]+ { yylval.val = atof(yytext); return(NUMBER); } [0-9]+\.[0-9]+ { sscanf(yytext,"%f",&yylval.val); return(NUMBER); } "+" return(PLUS); "-" return(MINUS); "*" return(MULT); "/" return(DIV); "^" return(EXPON); "(" return(LB); ")" return(RB); \n return(EOL); . { yyerror("Illegal character"); return(EOL); } %%
上記のファイルは、 以下のようにしてコンパイルすることができます。
bison -d -y expr.y flex -I expr.lex cc -o expr y.tab.c lex.yy.c alloca.c
また、 この例のソースが手元にあれば、 `examples'サブディレクトリにおいて`make expr'を実行するだけでコンパイルできます。 どちらの方法でも、 `expr'という名前の簡単な計算機が生成されます。 これは以下のような表現式をパースして、 その結果を出力します。
1 + 2 * (199*2)
これを見てお分かりのように、 この種のインターフェイスは非常に柔軟であり、 かつ、 保守も非常に容易です。 (トークンを定義する名前が変わらない限り) BisonとFlexの間のインターフェイスを変更することなく、 Flex、 Bisonいずれの入力情報においても、 機能の追加や削除、 定義やコードの変更を行うことが可能です。
この例では、
FlexとBisonの間で情報を渡すための別の方法を導入していることに注意してください。
この例では、
数字の値をBisonに返すのにyylval
を使っています。
これについては次の節でより詳細に説明します。
ここではとりあえず、
return
文の使い方を学んでおいてください。
注:これは単純な例です。 表現式のパース処理についてより詳しく知りたい人は、 Bisonのマニュアルを参照してください。
FlexからBisonに対して、 単なる整数値以上の情報を渡す必要の生じることがよくあります。 例えば、 コンパイラにおいては、 どのような種類のトークンが認識されたかだけではなく、 そのトークンの値についても知る必要のある場合がときどきあります。 文字列、文字、および数値定数などが良い例です。 ここで問題なのは、 どのようにしてFlexにこうした情報を返させるかです。
その答は、
Bisonが持っている%union
文です。
これは、
YYSTYPE
という型を定義するものです。
YYSTYPE
は、
パーサ定義中において使われるすべての正当なデータ型の共用体(union
)です。
Bisonがカレントなパース状態に関連づけたデータを保存するために使う、
YYSTYPE
型の変数yylval
というものがあり、
Flexからyylval
に値を設定することができるので、
トークンの型だけでなく、
それ以上の情報をBisonに返すことができます。
Bisonにおいて%union
を宣言して`-d'オプションを使うと、
Bisonは`.tab.h'という拡張子を持つファイルを作成して、
そこにトークンの定義情報だけでなく、
YYSTYPE
とyylval
の宣言も含めます。
したがって、
yylval
にアクセスするためにしなければならないことは、
Flexの定義情報の中にこの`.tab.h'ファイルをインクルードすることだけです。
これは、
追加のCコード・セクションにおける定義の先頭でインクルードしなければなりません
(FlexとBisonのインターフェイスを参照)。
注:初期のバージョンのBisonは、
自動的にYYSTYPE
とyylval
の宣言を生成しません。
この場合には、
より新しいバージョンのBisonを入手するか、
もしくは、
Flexの定義ファイルの先頭においてYYSTYPE
とyylval
を宣言する必要があります。
コードを読むのは、 プログラミングの方法を学ぶ良い方法です。 そこで、 Flex、Bisonのインターフェイス例をもう1つ示すことにします。 下の例では、 拡張してデータベースを操作するために使うことができるような、 小規模な言語のための簡単なパーサを作ります。
データベースとのインターフェイス言語は、 英語の非常に小さなサブセットになります。 文法はおおよそ以下のようになります。
command_list ::= sentence {sentence ...} sentence ::= verb_phrase noun_phrase position_phrase adverb period verb_phrase ::= VERB | adverb VERB noun_phrase ::= declared_noun | qualified_noun | noun declared_noun ::= declarator NOUN declarator ::= THIS | THAT | THE | THOSE qualified_noun ::= qualifier NOUN qualifier ::= SOME | MANY | ALL { declarator } NOUN position_phrase ::= position declarator NOUN | empty position ::= IN | ON | AT adverb ::= ADVERB | empty
結果として作成されるプログラムは、 以下のような文章を受け付けます。
FIND MEN QUICKLY FIND MEN FIND ALL MEN ON THE NETWORK QUICKLY FIND ALL MEN ON THE NETWORK FIND ALL MEN ON THE NETWORK QUICKLY
この例では、 BisonとFlexの間のインターフェイスが明確に示されるよう、 文章の簡単な解析結果が表示されます。 このプログラムを試しに実行してみると、 その表示結果は大体以下のようになります。
% front FIND MEN I understand that sentence. VP = FIND NP = MEN PP = AD = QUICKLY FIND ALL THE MEN ON THE NETWORK I understand that sentence. VP = QUICKLY FIND NP = ALL THE MEN PP = ON THE NETWORK AD = ^C %
これは特別便利なものではありません。
というのは、
これは文章の構成要素を表示する以外に何も行わないからです。
しかし、
そこには拡張のためのフックもありますし、
一般的な技法も示されています。
より一般的な形式の文章を受け付けるよう、
この例を拡張してみてください。
ほとんどの場合、
文章は動詞句(VERB
)と名詞句(NOUN
)に分割することができますが、
所有格名詞、
名詞の後ろに名詞が続く場合など、
文章を構成する他の要素も許容されるようにする必要があります。
(`FIND ALL JONE'S CAT NAMES'のような文章をどうやってパースするかを想像してみてください。)
Bisonの文法やその使い方に関する詳しい説明については、
Bisonのマニュアルを参照してください。
上の節で、 小規模な言語について説明しました。 次にそれを実装してみることにしましょう。 以下のファイルがこれを実現します。
注:これはあくまでも1つの例として見てください。 特に文法の部分は、 英語のパース処理としてはあまり良い例ではありません。
以下はBisonのファイルです。
%union
の部分、
および、
yylval
にアクセスするために$$
と$
nを使う方法に注目してください。
/* Cコードはファイルの先頭で提供する */ %{ #include <stdio.h> #include <string.h> extern int yylexlinenum; /* lex.yy.cに存在する */ extern char *yytext; /* カレント・トークン */ %} /* キーワードと予約語がここから始まる */ %union{ /* これはデータの共用体 */ char name[128]; /* 名前 */ } /*------------- 予約語 ------------------*/ %token PERIOD %token NEWLINE %token POSITIONAL %token VERB %token ADVERB %token PROPER_NOUN %token NOUN %token DECLARATIVE %token CONDITIONAL %type <name> declarative %type <name> verb_phrase %type <name> noun_phrase %type <name> position_phrase %type <name> adverb %type <name> POSITIONAL VERB ADVERB PROPER_NOUN %type <name> NOUN DECLARATIVE CONDITIONAL %% sentence_list : sentence | sentence_list NEWLINE sentence ; sentence : verb_phrase noun_phrase position_phrase adverb period { printf("I understand that sentence.\n"); printf("VP = %s \n",$1); printf("NP = %s \n",$2); printf("PP = %s \n",$3); printf("AD = %s \n",$4); } | { yyerror("That's a strange sentence!"); } ; position_phrase : POSITIONAL declarative PROPER_NOUN { sprintf($$,"%s %s %s",$1,$2,$3); } | /* 空 */ { strcpy($$,""); } ; verb_phrase : VERB { strcpy($$,$1); strcat($$," "); } | adverb VERB { sprintf($$,"%s %s",$1,$2); } ; adverb : ADVERB { strcpy($$,$1); } | /* 空 */ { strcpy($$,""); } ; noun_phrase : DECLARATIVE NOUN { sprintf($$,"%s %s",$1,$2); } | CONDITIONAL declarative NOUN { sprintf($$,"%s %s %s",$1,$2,$3); } | NOUN { strcpy($$,$1); strcat($$," "); } ; declarative : DECLARATIVE { strcpy($$,$1); } | /* 空 */ { strcpy($$,""); } ; period : /* 空 */ | PERIOD ; %% /* main()およびyyerror()関数を提供する */ void main(int argc, char **argv) { yyparse(); /* ファイルをパースする */ } int yyerror(char *message) { extern FILE *yyout; fprintf(yyout,"\nError at line %5d. (%s) \n", yylexlinenum,message); }
以下はFlexのファイルです。 文字列が渡される方法に注意してください。 これは最適化された方法ではありませんが、 最も理解しやすい方法です。
%{ #include <stdio.h> #include <string.h> #include "y.tab.h" /* これはBisonにより生成される */ #define TRUE 1 #define FALSE 0 #define copy_and_return(token_type) \ { \ strcpy(yylval.name,yytext);\ return(token_type); \ } int yylexlinenum = 0; /* 行数カウント用 */ %} %% /* 字句解析ルールがここから始まる */ MEN|WOMEN|STOCKS|TREES copy_and_return(NOUN) MISTAKES|GNUS|EMPLOYEES copy_and_return(NOUN) LOSERS|USERS|CARS|WINDOWS copy_and_return(NOUN) DATABASE|NETWORK|FSF|GNU copy_and_return(PROPER_NOUN) COMPANY|HOUSE|OFFICE|LPF copy_and_return(PROPER_NOUN) THE|THIS|THAT|THOSE copy_and_return(DECLARATIVE) ALL|FIRST|LAST copy_and_return(CONDITIONAL) FIND|SEARCH|SORT|ERASE|KILL copy_and_return(VERB) ADD|REMOVE|DELETE|PRINT copy_and_return(VERB) QUICKLY|SLOWLY|CAREFULLY copy_and_return(ADVERB) IN|AT|ON|AROUND|INSIDE|ON copy_and_return(POSITIONAL) "." return(PERIOD); "\n" yylexlinenum++; return(NEWLINE); . %%
これらのファイルは、 以下を実行することでコンパイルできます。
% bison -d front.y % flex -I front.lex % cc -o front alloca.c front.tab.c lex.yy.c
または、 この例のソースが手元にあれば、 `examples'サブディレクトリにおいて`make front'を実行することでもコンパイルできます。
注:Bisonパーサは`alloca.c'というファイルを必要とします。
このファイルはexamplesサブディレクトリにあります。
Bisonの代わりにyacc
を使うのであれば、
このファイルは必要ありません。
以下に実装に関する注を示します。
YYSTYPE
と yylval
yylval
がFlexからアクセスされる方法に注目してください。
Bison文法においてパース・ツリーの上位にデータを渡す方法については、
Bisonのマニュアルに説明されていますが、
Flexに対しては何の影響も持ちません。
整数値、浮動小数点数値、および他の任意の型のデータも同様の方法で返すことができます。
yylex()
を呼び出し、
スキャナがトークン定義を提供しています。
Flex 2.5では、 Flexに対するC++インターフェイスが提供されています。
FlexのC++インターフェイスを使うためには、 Flex実行時に`-+'オプションを指定するか、 スキャナ定義ファイルの中で`%option c++'を指定する必要があります。 これにより、 C++のスキャナ・クラスを実装する`lex.yy.cc'というファイルが生成されます。
`lex.yy.cc'は、
Flexが提供する`FlexLexer.h'をインクルードします。
この`FlexLexer.h'の中に、
C++スキャナ・クラスの実装に利用される2つのC++クラス
(FlexLexer
とyyFlexLexer
)
が定義されています。
FlexLexer
は、
C++スキャナ・クラスが実装すべきインターフェイスを構成する抽象仮想関数を定義するクラスです。
FlexLexer
の持つメンバを以下に示します。
char* yytext
int yyleng
int yylineno
int yy_flex_debug
次に、
FlexLexer
の持つメンバ関数のうち、
抽象仮想関数ではないものを以下に示します。
const char* YYText()
yytext
の値を返します。
int YYLeng()
yyleng
の値を返します。
int yylex(istream* new_in, ostream* new_out = 0)
switch_streams()
を呼び出した後、
メンバ関数int yylex(void)
を呼び出します。
int lineno() const
yylineno
の値を返します。
int debug() const
yy_flex_debug
の値を返します。
void set_debug(int flag)
yy_flex_debug
に代入します。(12)
次に、
FlexLexer
の持つ抽象仮想メンバ関数を列挙します。
void yy_switch_to_buffer(struct yy_buffer_state* new_buffer) struct yy_buffer_state* yy_create_buffer(istream* s, int size) void yy_delete_buffer(struct yy_buffer_state* b) void yyrestart(istream* s) int yylex() void switch_streams(istream* new_in = 0, ostream* new_out = 0)
最初の5つのメンバ関数は、
FlexのCインターフェイスにおける同名の関数と同等の機能を実現します。
Cインターフェイスでは、
FILE*
となっていた引数の型が、
istream*
となっている点に注意してください。
最後のswitch_streams()
は、
入出力ストリームの切り替えを行います。
これらの抽象仮想メンバ関数の定義は、
サブクラスyyFlexLexer
において与えられ、
そのコードは`lex.yy.cc'の中に生成されます。
yyFlexLexer
は、
FlexLexer
のサブクラスです。
デフォルトの状態では、
yyFlexLexer
のインスタンスを生成して、
yylex()
メンバ関数を呼び出すことによって、
スキャナの処理が実行されます。
以下に例を示します。
int main( int /* argc */, char** /* argv */ ) { FlexLexer* lexer = new yyFlexLexer; while(lexer->yylex() != 0) ; return 0; }
これは、 Cインターフェイスにおける、 以下のコードに対応します。
int main( int /* argc */, char** /* argv */ ) { yylex(); return 0; }
スキャナ定義ファイルの中に`%option yyclass="classname"'を指定すると、
`lex.yy.cc'にclassname::yylex()
が生成されます。
クラスclassnameをyyFlexLexer
のサブクラスとして定義することによって、
classnameのインスタンスを使ってスキャン処理を実行することができます。
クラスclassnameを定義する際、
以下に示す、
yyFlexLexer
の持つprotected
メンバ関数を再定義することによって、
スキャナの振る舞いを変更することができます。
int LexerInput(char* buf, int max_size)
#ifdef YY_INTERACTIVE
を使います。
void LexerOutput(const char* buf, int size)
void LexerError(const char* msg)
スキャン処理に関わるすべてのコンテキスト情報は、
yyFlexLexer
のインスタンスの内部に閉じています。
このことは、
C++スキャナ・クラスを使うことによって、
再入可能なスキャナを生成することが可能であることを意味しています。
複数のC++スキャナ・クラスを生成して、
1つの実行プログラムにリンクすることも可能です。
これを行うには、
Flex起動時に`-Pprefix'オプションを指定するか、
スキャナ定義ファイルの中に`%option prefix="prefix"'を指定することによって、
yyFlexLexer
の名前を`prefixFlexLexer'に変更します。
prefixFlexLexer
クラスを使うソース・ファイルの中では、
以下のようにして`FlexLexer.h'をインクルードすることによって、
prefixFlexLexer
(実際にはyyFlexLexer
)
の定義を参照する必要があります。
#undef yyFlexLexer #define yyFlexLexer prefixFlexLexer #include <FlexLexer.h>