プログラミングユーティリティ

lex ソースの作成

lex のソースは、3 つまでのセクションで構成されています。その 3 つとは、定義、規則、ユーザー定義ルーチンです。規則セクションは必須です。定義とユーザー定義ルーチンのセクションは省略できますが、使用する場合には、以下に示した順に配置する必要があります。

定義
%% 
規則
%% 
ユーザー定義ルーチン

lex 規則の基礎

必須の規則セクションは、区切り記号 %% で開始します。ルーチンのセクションを後に続ける場合は、規則の最後の行に %% を記述して規則のセクションをそこで終了します。区切り記号 %% は、行の先頭に入力する必要があります。%% の前に空白文字は入れないでください。2 つ目の区切り記号がない場合には、規則セクションがプログラムの終わりまで続いているとみなされます。

規則セクション内の最初の規則の前にある空白で始まる行は、関数 yylex() の開始部分の最初の中括弧のすぐ後ろにコピーされます。この機能を利用すれば、yylex() の局所変数を宣言できます。

各規則によって、検索するパターンと、そのパターンが見つかったときに実行するアクションが指定されます。パターン仕様は、行の先頭から入力する必要があります。スキャナは、パターンと一致しない入力を出力ファイルに直接書き込みます。したがって、最も単純な字句アナライザプログラムは、規則セクションの開始を示す区切り記号 %% だけが入っているプログラムです。このプログラムは、すべての入力をまったく変更を加えずにそのまま出力に書き込みます。

正規表現

検索するパターンは、正規表現と呼ばれる表記法を使用して指定します。正規表現は、文字や演算子をつなぎ合せて形成します。最も単純な正規表現は、以下のような演算子が 1 つも含まれていないテキスト文字の列です。

apple 
orange
pluto 

これら 3 つの正規表現は、入力テキストに現われるこれと同じ文字列すべてと一致します。入力テキストに現われる orange をスキャナにすべて削除させたい場合には、以下のように規則を指定することができます。

orange ;

セミコロンの後にアクションが指定されていないため、スキャナは何も行わずに、元の入力テキストからこの正規表現に一致した文字列をすべて削除したテキストを出力します。つまり、出力されるテキストには、文字列 orange はまったく含まれません。

演算子

ほとんどの式は、orange のように簡単に指定することはできません。式自体は非常に長いものであったり、あるいは (こちらの方がよくある事例ですが) その式のクラスが非常に大きい可能性があります。実際、無限の大きさである可能性も考えられます。

lex 演算子 (表 2-1 を参照) を使用して、特定のクラスのあらゆる式を表すための正規表現を形成できます。たとえば + 演算子は、直前の式が 1 個以上存在することを表します (つまり、直前の式は省略可能です)。? は、直前の式が 0 個または 1 個存在することを表します。* は、直前の式が 0 個以上存在することを表します。したがって m+ は、1 個以上の m で構成される文字列すべてと一致する正規表現になります。

mmm 
m 
mmmmm 

また、7* は、0 個以上の 7 で構成される文字列すべてと一致する正規表現となります。

77 
77777 

777

空白の 3 行目が正規表現に一致したのは、その行に 7 が 1 つも含まれていないためです。

| 演算子は、二者択一を表します。したがって、ab|cdab または cd と一致します。演算子 {} は、繰り返しを指定します。したがって a{1,5} は、a が 1 〜 5 個含まれている文字列と一致します。A(B{1,4}) は、ABCABBCABBBCABBBBC と一致します (グループ化するための記号として丸括弧 ()を使用している点に注目してください)。

角括弧 [] は、角括弧内に指定された文字列のいずれかの文字を表します。したがって [dgka] は、1 個の dgk、または a と一致します。角括弧内の文字は、間に空白文字や句読点を入れないで隣接して並べてください。

^ 演算子は、左角括弧の直後の文字として使用したときには、角括弧内に指定した文字を除く標準セットのすべての文字を表します。|{}、および ^ は、他の目的で使用する場合もあるので注意してください。

標準のアルファベットや数字 (A 〜 Z、a 〜 z、0 〜 9) の範囲は、ハイフンを使用して指定します。たとえば、[a-z] は、任意の小文字を表します。

[A-Za-z0-9*&#]

これは、任意の文字 (大文字または小文字)、任意の数字、アスタリスク、アンパサント、または # と一致する正規表現の式です。

以下に示した入力テキストを与えられると、上記の仕様を規則の 1 つとして持つ字句アナライザは、*&r# を認識して、規則が指定するアクションをその認識した各文字に対して実行し、残りのテキストをそのまま出力します。

$$$$?? ????!!!*$$ $$$$$$&+====r‾‾# (( 

ハイフン文字をクラスに加えるには、[-A-Z] または [A-Z-] のように、角括弧内の最初または最後の文字として入力してください。

演算子は、組み合わせて使用すると非常に効果的です。たとえば、複数のプログラミング言語の識別子を認識するための正規表現は、次のようになります。

[a-zA-Z][0-9a-zA-Z]*

これらの言語における識別子は、文字の後にゼロ、文字、または数字が続く文字列として定義されており、上記の正規表現は、その識別子を表しています。 1 組目の角括弧は、任意の文字に一致します。2 組目の角括弧は、その後の * がなければ、任意の数字または文字に一致します。

2 つの角括弧の組とそれに囲まれた文字は、後ろに 1 個の数字または文字が続く任意の文字と一致します。しかし * が付いているため、この例は後ろに任意の数の文字または数字が続く任意の文字と一致します。たとえば、この例は以下の文字列を識別子として認識します。

e 
not 
idenTIFIER 
pH 
EngineNo99 
R2D2

一方、以下の文字列は識別子として認識されません。not_idenTIFIER には下線が含まれており、5times は文字でなく数字で始まっており、$hello は特殊文字で始まっているためです。

not_idenTIFIER
5times 
$hello 

演算子文字に潜在する問題は、検索する文字としてどのようにその演算子文字を検索パターン内で指定するかということです。たとえば、前述の例は * を含むテキストを認識しません。lex では、2 つの方法のいずれかでこの問題を解決します。バックスラッシュの後の演算子文字、二重引用符で囲まれた文字 (バックスラッシュは除く) は、そのまま文字として扱われます。つまり、検索されるテキストの一部となります。

たとえば、バックスラッシュによる方法を使用して、* の後に任意の数の数字が続く文字列を認識するには、以下のパターンを使用できます。

¥*[1-9]*

¥ 自体を認識するには、バックスラッシュを 2 つ並べる必要があります (¥¥)。同様に、"x¥*x"x*x と一致し、"y¥"z"y"z と一致します。その他の lex 演算子については、表 2-1 で説明しています。

表 2-1 lex 演算子

式 

説明 

¥x

x が lex 演算子の場合は x

"xy"

x または y が lex 演算子 (¥ は除く) の場合でも xy

[xy]

x または y 

[x-z]

x、y、または z 

[^x]

x 以外の文字 

.

復帰改行以外の文字 

^x

行の先頭の x 

<y>x

lex が開始状態 y にあるときは x

x$

行の最後の x 

x?

0 個または 1 個の x 

x*

0 個以上の x 

x+

1 個以上の x 

x{m,n}

m 〜 n 個の x 

xx|yy

xx または yy 

x |

x に対するアクションは、次の規則のアクション 

(x)

x/y

x の後に y が続く場合にのみ x 

{xx}

定義セクションからの xx の変換 

アクション

スキャナは、規則の開始部にある正規表現に一致する文字列を認識すると、その規則の右側を検査してアクションを実行します。ユーザーは、このアクションを指定します。

見つかったトークンの型とその値を記録する、トークンを別のトークンと置換する、トークンまたはトークンの型のインスタンス数 (出現する数) をカウントする、などがアクションとして考えられます。これらのアクションは、C のプログラムとして作成します。

アクションは、任意の数の文を使用して構成できます。何らかの方法でテキストを変更したり、テキストが見つかったことを示すメッセージを出力したい場合があります。たとえば、式 Amelia Earhart を認識して、認識したことを通知するには、以下の規則を適用します。

"Amelia Earhart" printf("found Amelia"); 

テキスト内の長い医学用語を同義の略語に置換する処理は、以下のような規則で実現できます。

Electroencephalogram printf("EEG"); 

テキスト内の行数をカウントするには、行の終わりを認識して、行カウンタを増分します。

lex では、復帰改行を表す ¥n などの、標準の C エスケープシーケンスを使用します。たとえば行数をカウントするには、以下のような構文を使用できます。ここで使用している lineno は、他の C 変数と同じようにして、「定義」で宣言されています。

¥n  lineno++; 

C 言語の NULL 文 (1 個のコロン ;) を指定すると、入力は無視されます。したがって、以下の規則を使用した場合には、空白文字、タブ、復帰改行は無視されます。

[ ¥t¥n] ; 

また、択一演算子 | は、ある規則とその次の規則のアクションが同じであることを表す際に使用することもできます。たとえば、前述の例は以下のように記述しても同じ結果になります。

" " | 
¥t
¥n ;

スキャナは、式と一致したテキストを yytext[] という文字配列内に格納します。必要に応じて、この配列の内容を出力したり操作できます。実際に、lexprintf ("%s", yytext) と同義の ECHO というマクロを備えています。

C 言語の長い文または複数の文でアクションを構成する場合には、その文を複数の行に渡って記述できます。アクションが、1 つの規則だけに対するものであることを lex に伝えるには、その C コードを中括弧で囲みます。

たとえば、数字列が見つかるごとにその数字列とそれまでに見つかった数字列の総数を出力しながら、入力テキスト全体の数字列の総数をカウントするには、次のような lex コードを使用します。

¥+?[1-9]+      { digstrngcount++; 
                 printf("%d",digstrngcount); 
                 printf("%s", yytext); }

? は、その直前の文字が 0 個または 1 個存在することを表しているので、この仕様は 0 個または 1 個のプラス記号が直前に付いた数字列と一致します。また、負の数字列の場合でも、マイナス記号の後の部分はこの仕様に一致するので、同様に数値が捕捉されます。

lex の高度な機能

lex に提供されている一連の機能を使用することによって、複雑なパターンで入力テキストを処理できます。考えられる複数の仕様のうちどの仕様が一番適しているかを決定する規則、一致パターンを別の一致パターンに変換する関数、定義とサブルーチンの使用なども含まれています。

以下の例には、これまで取り上げてきた事項がまとめて記述されています。

%% 
-[0-9]+     printf("negative integer"); 
¥+?[0-9]+   printf("positive integer"); 
-0.[0-9]+   printf("negative fraction, no whole number part");
rail[ ¥t]+road   printf("railroad is one word"); 
crook       printf("Here's a crook"); 
function    subprogcount++; 
G[a-zA-Z]*  { printf("may have a G word here:%s", yytext); 
                      Gstringcount++; }

最初の 3 つの規則では、それぞれ負の整数、正の整数、0 〜 -1 の間の負の小数が認識されます。各仕様の終わりには + が付いているので、これらの数字は 1 桁または複数桁の数字で構成されることになります。

その後の規則は、それぞれ以下に示す特定のパターンを認識します。

特殊な機能

スキャナは、一致した入力テキストを yytext[] に格納する以外にも、一致した入力テキストの文字数をカウントしてその値を変数 yyleng に自動的に格納します。ユーザーは、この変数を使用して、配列 yytext[] に配置された任意の文字を参照できます。

C 言語の配列インデックスは 0 から始まります。したがって、認識した整数の 3 桁目の数字 (3 桁目の数字がある場合) を出力するには、以下のように記述します。

[1-9]+    {if (yyleng > 2) 
             printf("%c", yytext[2]); 
           } 

記述した一連の規則によってあいまいさが生じた場合、lex は上位のルールに従ってそのあまいさを解決します。以下の字句アナライザの例では、「予約語」end は 2 番目の規則と、識別子用の 8 番目の規則に一致します。

begin       return(BEGIN); 
end         return(END); 
while       return(WHILE); 
if          return(IF); 
package     return(PACKAGE); 
reverse     return(REVERSE); 
loop        return(LOOP); 
[a-zA-Z][a-zA-Z0-9]*  { tokval = put_in_tabl();
                         return(IDENTIFIER); }
[0-9]+   { tokval = put_in_tabl();
             return(INTEGER); }
¥+       { tokval = PLUS;
            return(ARITHOP); } 
¥-       { tokval = MINUS;
            return(ARITHOP); }
>        { tokval = GREATER;
            return(RELOP); } 
>=       { tokval = GREATEREQL;
            return(RELOP); } 

lex は、仕様内の 2 個以上の規則と一致する場合には、最初の規則のアクションが実行されるというルールに従います。識別子用の規則の前に end やその他の予約語用の規則を配置すれば、予約語を確実に認識できます。

検索するパターンが別のパターンの接頭辞になっている場合には、また別の問題が発生する可能性があります。たとえば、上記の字句アナライザの例における最後の 2 つの規則は、>>= を認識するようにデザインされています。

lex は、できる限り長い文字列と照合してその文字列用の規則を実行する、というルールに従います。テキストの一部に文字列 >= が含まれている場合には、スキャナは > で止まって > の規則を実行するのではなく、>= を認識して >= の規則を実行します。このルールによって、C プログラムの +++ が区別されます。

字句アナライザが、検索する文字列を超えて読み取る必要がある場合には、後方コンテキストを使用してください。その典型的な例が、FORTRAN の DO 文です。以下の DO 文の最初の 1 は、最初のコンマを読み取るまではインデックス k の初期値のように見えます。

DO 50 k = 1 , 20, 1 

つまり、最初のコンマを読み取るまでは、この文は代入文のように見えます。

DO50k = 1 

FORTRAN では空白はすべて無視されます。後ろに続く文字列が後方コンテキストであることを示すには、スラッシュ / を使用します。スラッシュはパターンの一部ではないため、この後方コンテキストは yytext[] には格納されません。

FORTRAN の DO 文を認識する規則は、以下のように記述できます。

DO/([ ]*[0-9]+[ ]*[a-zA-Z0-9]+=[a-zA-Z0-9]+,) { 
        printf("found DO");
        }

別バージョンの FORTRAN で識別子 (この例ではインデックス名) のサイズが制限されていても、この規則のように任意の長さのインデックス名を受け入れると、記述を簡素化することができます。後方コンテキストと類似した前方コンテキストの取り扱いについては、「開始条件」を参照してください。

lex では、特別な後方コンテキスト (行の終わり) を表す演算子として $ 記号を使用します。例として、行の終わりの空白文字とタブをすべて無視する規則を以下に示します。

[ ¥t]+$ ; 

上記の例は、以下のように記述することもできます。

[ ¥t]+/¥n ; 

パターンが行またはファイルの先頭にあるときにだけそのパターンと一致させるには、^ 演算子を使用します。たとえば、あるテキスト書式整形プログラムでは、空白文字以外の文字で行を開始する必要があるとします。その場合には、以下の規則を使用して、そのプログラムへの入力を検査できます。

^[ ]    printf("error: remove leading blank"); 

左角括弧の内側に ^ 演算子がある場合との意味の違いに注意してください。

lex ルーチン

以下のマクロを使用して特殊なアクションを実行できます。

2 つの特殊文字 (たとえば、二重引用符など) の間にある文字をすべて無視する方法の 1 つとして、以下のように input() を使用する方法があります。

¥"      while (input() != '"'); 

スキャナは、最初の二重引用符を読み取ってから 2 番目の二重引用符を読み取るまでは、照合を行わずに後続の文字をすべて読み取ります。(input()unput() の使い方の詳しい例については、「ユーザールーチン」を参照してください。)

これらデフォルトのマクロでは対応していない特殊な入出力 (複数ファイルへの書き込みなど) が必要な場合には、C の標準入出力ルーチンを使用してマクロ関数を書き直してください。

ただし、これらのルーチンは一貫性をもって変更する必要があります。特に、文字セットはすべてのルーチンで同じものを使用し、input() によって返される値 0 はファイルの終わりを意味していなければなりません。また、input()unput() との間の関係も維持する必要があります。関係が維持されていないと、lex の先読み処理は機能しなくなります。

独自の input()output(c)、または unput(c) を作成する場合には、まず初めに #undef input#undef output、または #undef unput を定義セクションに記述してください。

#undef input
#undef output
  . . .
#define input() ...  etc.
more declarations 
  . . .

標準のルーチンが新しいルーチンへ置換されます。詳しくは、「定義」を参照してください。

ユーザーが再定義できる lex ライブラリルーチンは、スキャナがファイルの終わりに到達すると必ず呼び出される yywrap() です。yywrap() が 1 を返した場合には、スキャナは入力の終わりで通常の後処理を行います。新しいソースから引き続き入力を受け取るようにするには、yywrap() を再定義して、継続して処理が必要なときには 0 を返すようにします。デフォルトの yywrap() は必ず 1 を返します。

ファイルの終わりを認識する標準の規則を記述することはできないので注意してください。yywrap() からのみ、ファイルの終わりを認識する標準の規則を変更することができます。input() によって返される値 0 はファイルの終わりとみなされるので、ユーザーが独自の input() を用意しないかぎり、NULL を含んだファイルを取り扱うことはできません。

複数の方法で処理される文字列を扱うための lex ルーチンとしては、yymore()yyless(n)REJECT があります。 与えられた仕様に一致したテキストは配列 yytext[] に格納されることに注意してください。一般に、その仕様のアクションが実行されると、yytext[] 内の文字は、その次に一致した入力ストリーム内の文字に置換されます。

一方、関数 yymore() の場合には、認識された後続の文字は yytext[] 内にすでに格納されている文字の後に追加されます。この関数を使用すれば、ある文字列が有効で、その文字列を含んだ別の文字列も有効なときなどに、処理を連続して行うことができます。

二重引用符で囲まれた文字の集まりとして文字列が定義されている言語において、文字列に二重引用符を含める指定を行うには、その引用符の直前にバックスラッシュを置く必要があります。これに一致する正規表現は多少複雑であるため、次のように記述してください。

¥"[^"]* { 
     if (yytext[yyleng-2] == '¥¥') 
         yymore(); 
     else
         ... 通常処理
     }

スキャナは、文字列 "abc¥"def" に出会うと、まず文字列 "abc¥" と照合します。次に、yymore() を呼び出して、文字列の次の部分 "def を先の文字列の後ろに追加します。文字列の終わりを示す二重引用符は、「通常処理」と記されている部分のコードで取り扱われます。

関数 yyless(n) を使用して、アクションの実行対象となる一致文字の数を指定できます。yytext[] には、式の最初の n 文字だけが保持されます。後続の処理は、nth+1 番目の文字から再開されます。

たとえば、コードの解読を行なっていて、特定の文字 (この例では、小文字または大文字の Z) で終了する文字列の半分だけを処理する場合には、以下のように記述します。

[a-yA-Y]+[Zz] { yyless(yyleng/2); 
                 ... 文字列の前半分を処理
                 ...
                }

また、REJECT 関数を使用すれば、文字列が重複していたり、文字列の一部に別の文字列が含まれている場合であっても、文字列の処理をさらに容易に行うことができます。REJECT 関数では、yytext[] の内容を変更せずに次の規則とその仕様に直接ジャンプすることによって、この処理を実現しています。たとえば、入力テキスト内の正規表現 snapdragon とその部分式 dragon の両方の出現回数をカウントするには、以下のように記述します。

snapdragon    {countflowers++; REJECT;} 
dragon         countmonsters++;

パターンが別のパターンと重複しているときの例を以下に示します。この例では、入力テキストに comediana などの文字列が含まれている場合でも、式 comediandiana の出現回数をカウントします。

comedian    {comiccount++; REJECT;} 
diana        princesscount++; 

この例のアクションはカウンタの値を増やしていくことですが、それよりもずっと複雑なアクションを指定しなければならない場合もあります。カウンタやその他必要な変数は、常に lex 仕様の開始部分の定義セクションで宣言してください。

定義

lex の定義セクションには、複数のクラスの項目を入れることができます。最も重要なのは、外部定義、#include などの前処理文、および略語です。正式な lex ソースではこのセクションは省略可能ですが、ほとんどの場合において、これらの項目のいくつかは必要になります。前処理文と C ソースコードは、%{%} の行の間に記述します。

この区切り記号の間の行 (空白で始まる行も含む) は、すべて yylex() の定義の直前に lex.yy.c にコピーされます。(区切り記号で囲まれていない定義セクション内の行は、同じ場所にコピーされ、その先頭には空白が入れられます。)

定義セクションは通常、規則セクション内のアクションまたは外部にリンクされたルーチンによってアクセスされるオブジェクトの C 定義を配置する場所です。

たとえば、字句アナライザを呼び出すパーサーを生成する yacc と共に lex を使用するときには、ファイル y.tab.h をインクルードします。このファイル中で、#define を使用してトークン名を定義することができます。

%{ 
#include "y.tab.h"
extern int tokval; 
int lineno;
%} 

#include 文と宣言の終わりを示す %} の後には、規則セクション内の正規表現の略語を置いてください。行の左側には略語、行の右側にはその定義または変換後の正規表現を記述して、両者を 1 つ以上の空白文字で区切ります。

規則の中で略語を使用するときには、その略語を中括弧で必ず囲んでください。略語を使用することにより、仕様を繰り返し記述する手間が省け、見た目にも読みやすくなります。

たとえば、lex の高度な機能」で取り上げた lex ソースについて再検討します。定義を使用することによって、数字、文字、空白文字が後で見た時にわかりやすくなります。

これは、同じ仕様が何度も現われる場合に特に効果的です。

D                 [0-9]
L                 [a-zA-Z]
B                 [ ¥t]+
%%
-{D}+             printf("negative integer");
¥+?{D}+           printf("positive integer");
-0.{D}+           printf("negative fraction"); 
G{L}*             printf("may have a G word
                          here"); 
rail{B}road       printf("railroad is one word");
crook             printf("criminal"); 
 ...
 ...

開始条件

開始条件を使用すると、単独の ^ 演算子よりも細かく前方コンテキストを取り扱うことができます。たとえば、行の終わりまたはファイルの始まりよりも複雑な前方コンテキストに従って、異なる規則を式に適用する場合があります。

その場合には、規則の適用条件となるコンテキスト内の変化を示すフラグを設定して、そのフラグをテストするコードを作成できます。あるいは、各規則の適用条件となるさまざまな「開始条件」を lex 用に定義することもできます。

以下の問題について考えます。

開始条件を使用してこれと同じ問題を扱うには、以下のような行を lex の定義セクションに記述する必要があります。定義セクションには、任意の順序で条件を記述することができます。

%Start name1 name2 ...  

ワード Start は、S または s と短縮できます。この条件を参照する場合は、規則の先頭で角括弧 <> を使用します。たとえば、スキャナの開始条件が name1 のときにだけ認識される規則は、以下のようになります。

<name1>式

開始条件に入るには、以下のアクションの文を実行します。

BEGIN name1; 

上記の文は、開始条件を name1 に変更します。通常の状態に戻す場合は、以下の文を使用します。

BEGIN 0;

この文は、スキャナの初期条件をリセットします。

規則は、複数の開始条件によってアクティブにすることができます。たとえば、以下のような開始条件にすることもできます。

<name1,name2,name3> 

前置演算子 <> で開始されていない規則は、すべてアクティブです。

開始条件を使用した場合には、前述の例は以下のように記述できます。

%Start AA BB CC 
%% 
^a   {ECHO; BEGIN AA;} 
^b   {ECHO; BEGIN BB;} 
^c   {ECHO; BEGIN CC;} 
¥n   {ECHO; BEGIN 0;} 
<AA>magic   printf("first"); 
<BB>magic   printf("second"); 
<CC>magic   printf("third"); 

ユーザールーチン

ユーザーは、他のプログラミング言語でルーチンを使用する場合と同じようにして lex ルーチンを使用できます。複数の規則で使用されるアクションのコードは、1 度だけ作成して、必要なときに呼び出すことができます。定義の場合と同様に、このルーチンによってプログラムの記述が簡素化され、コードも読みやすくなります。

lexyacc の併用」で説明している put_in_tab1() 関数は、lex 仕様のユーザールーチンのセクションで使用するのに適した関数です。

このセクションにルーチンを配置するもう 1 つの理由は、コードを使用する規則が 1 つしかない場合であっても、重要なコードを強調したり規則セクションを簡素化するためです。例として、/**/ に挟まれた部分がコメントになっている C のような言語のコメントを無視する以下のルーチンについて説明します。

%{ 
static skipcmnts(); 
%} 
%% 
"/*"       skipcmnts(); 
 ...
 ...    /* 残りの規則 */
%% 
static 
skipcmnts() 
{ 
       for(;;) 
       { 
            while (input() != '*') 
                  ; 
         
            if (input() != '/') 
                unput(yytext[yyleng-1]) 
            else return; 
       } 
}

この例には、重要なポイントが 3 つあります。