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

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 つあります。