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

エラー処理

エラー処理には、意味上の問題がいくつか含まれています。たとえば、エラーが見つかったときに、構文解析ツリーの記憶領域の再生、シンボルテーブルのエントリの削除や変更、スイッチを設定して出力の生成を止めたりしなければならない場合があります。

エラーが見つかったときに処理をすべて停止するのは、あまり好ましくありません。入力の検査を継続してその先の構文エラーを見つける方が便利です。その場合には、エラーの後でパーサーを再始動させるという問題が持ち上がります。再始動を行う一般的な部類のアルゴリズムには、入力文字列からトークンをいくつか破棄する処理や、入力を継続するためにパーサーを調整しようとする、という処理が含まれています。

この処理の一部をユーザーが制御できるようにするため、yacc はエラーにトークン名 error を設定します。この名前は、構文規則の中で使用できます。結果的に、この名前は、エラーの発生する箇所と、エラー回復を行うことができる箇所を示します。

パーサーは、トークンエラーが有効な状態に入るまで、そのパーサーのスタックをポップします。そして、トークンエラーが現在の lookahead トークンであるかのようにふるまって、次のアクションを実行します。その後、lookahead トークンはエラーを引き起こしたトークンにリセットされます。エラー規則が特に指定されていない場合は、エラーの検出時に処理は停止します。

エラーメッセージが連続して出力されるのを避けるため、パーサーは、エラー検出後に 3 つのトークンを正常に読み取ってシフトするまではエラー状態を継続します。すでにパーサーがエラー状態にあるときにエラーを検出した場合には、メッセージは表示されず、入力トークンが削除されます。

たとえば、以下の形式の規則は、構文エラー時にパーサーはエラーが見つかった文を読み飛ばそうとすることを表しています。

stat : error

正確に言うと、パーサーは前方を検査し、正当に文の後に続いていると考えられる 3 つのトークンを探して、その 3 つの内の最初のトークンの処理を開始します。文の開始部分が明確でない場合は、誤って文の途中から処理を開始して、実際にはエラーではないところで 2 つ目のエラーを報告することがあります。

アクションは、これらの特別なエラー規則と共に使用できます。これらのアクションは、テーブルの再初期化、シンボルテーブル空間の修正などを試みます。上記のようなエラー規則は、非常に一般的な規則ですが、制御するのは困難です。

以下のような規則であれば、制御は多少容易になります。

stat : error ';'

この規則の場合は、エラーが見つかるとパーサーは文を次のセミコロンまで読み飛ばします。エラー後の次のセミコロンより前のトークンは、シフトできず、すべて破棄されます。セミコロンが見つかると、この規則は還元されてそれに関連するアクションが実行されます。

もう 1 つの形式は、エラーの後に行を再入力できるようにした方が望ましい対話型アプリケーションで用いられます。それを行う方法の 1 つを以下に示します。

input     : error '¥n' 
           { 
               (void) printf("Reenter last line: " ); 
           } 
           input 
           { 
               $$ = $4; 
           } 
;

この方法には、潜在的な問題が 1 つあります。パーサーは、エラーの後で正しく再同期したことを確認する前に 3 つの入力トークンを正しく処理しなければなりません。再入力された行の最初の 2 つのトークンにエラーが含まれている場合には、パーサーはその不正なトークンを削除し、何もメッセージは表示しません。これは、好ましくない状態です。

そのために、エラーが回復されたことをパーサーに認識させるメカニズムが用意されています。アクションの中に以下の文があると、パーサーは通常のモードにリセットされます。

yyerrok ;

したがって、前述の例は、以下のように書き直すことができます。

input      : error '¥n' 
            { 
                yyerrok; 
                (void) printf("Reenter last line: " ); 
            } 
            input 
            {
                $$ = $4; 
            } 
            ;

エラーシンボルの直後に認識されたトークンは、エラーが発見された入力トークンです。場合によっては、このことが不適切になることがあります。たとえば、エラー回復アクションは、入力を再開する正しい位置を探すジョブを実行する必要があるとします。その場合には、1 つ前の lookahead トークンを消去する必要があります。アクションに以下の文を記述すれば、そのトークンは消去されます。

yyclearin ;

たとえば、次の有効な文の先頭まで入力を進める (ユーザーが作成した) 複雑な再同期化ルーチンの呼び出しが、エラー後のアクションであると仮定します。このルーチンを呼び出した後は、yylex() によって返された次のトークンは、有効な文の最初のトークンであるとみなされます。古い無効なトークンは破棄し、エラー状態をリセットする必要があります。これを実行する規則の例を以下に示します。

stat      : error 
          { 
              resynch(); 
              yyerrok ; 
              yyclearin; 
          } 
          ;

これらのメカニズムは、確かに不完全ですが、簡単かつ効果的にパーサーを多くのエラーから回復できます。また、ユーザーは、プログラムの他の部分で必要なエラーアクションの処理を制御することもできます。