レクサーは、メインパーサーのパフォーマンス最適化として使用される単純なパーサーです。レクサーがある場合、レクサーとパーサーは連携して完全な言語を記述します。個別の字句解析ステージを持たないパーサーは、「スキャナーなし」と呼ばれることもあります。
レクサーがなければ、パーサーは文字ごとに操作する必要があります。パーサーはすべての入力項目に関するメタデータを保存する必要があり、入力項目の状態ごとにテーブルを事前に計算する必要があるため、大きな入力サイズでは許容できないメモリ消費が発生します。特に、抽象構文ツリーの文字ごとに個別のノードは必要ありません。
文字ごとのテキストはかなりあいまいであるため、これはまた、処理するのが面倒なはるかにあいまいな結果になります。ルールを想像してくださいR → identifier | "for " identifier
。どこ識別子は、 ASCII文字から構成されています。あいまいさを避けたい場合は、4文字の先読みを使用して、どの選択肢を選択すべきかを判断する必要があります。字句解析器を使用すると、パーサーはIDENTIFIERまたはFORトークン(1トークンの先読み)を持っているかどうかを確認するだけです。
2レベルの文法。
レクサーは、入力アルファベットをより便利なアルファベットに変換することで機能します。
スキャナーレスパーサーは、文法(N、Σ、P、S)を記述します。ここで、非終端記号Nは文法の規則の左側、アルファベットΣはたとえばASCII文字、プロダクションPは文法の規則です、開始記号Sはパーサーの最上位ルールです。
レクサーは、トークンa、b、c、…のアルファベットを定義するようになりました。これにより、メインパーサーはこれらのトークンをアルファベットとして使用できます:Σ= {a、b、c、…}。字句解析器の場合、これらのトークンは非終端記号であり、開始規則S LはS L →ε| S | b S | c S | …、つまり:トークンのシーケンス。字句解析器の規則はすべて、これらのトークンを生成するために必要な規則です。
パフォーマンスの利点は、レクサーのルールを通常の言語として表現することです。これらは、コンテキストフリー言語よりもはるかに効率的に解析できます。特に、通常の言語はO(n)スペースとO(n)時間で認識できます。実際には、コードジェネレーターはそのようなレクサーを非常に効率的なジャンプテーブルに変えることができます。
文法からトークンを抽出します。
あなたの例に触れるには:digit
とstring
ルールは文字ごとのレベルで表現されます。それらをトークンとして使用できます。残りの文法はそのままです。以下は、正規であることを明確にするために右線形文法として書かれた字句解析文法です。
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"' , string-rest ;
string-rest = '"' | STRING-CHAR, string-rest ;
STRING-CHAR = ? all visible characters ? - '"' ;
しかし、これは規則的であるため、通常は正規表現を使用してトークンの構文を表現します。上記の正規表現としてのトークン定義は、.NET文字クラス除外構文とPOSIX文字クラスを使用して記述されています。
digit ~ [0-9]
string ~ "[[:print:]-["]]*"
メインパーサーの文法には、レクサーで処理されない残りのルールが含まれます。あなたの場合、それはただ:
input = digit | string ;
レクサーを簡単に使用できない場合。
言語を設計するときは、通常、文法をレクサーレベルとパーサーレベルにきれいに分離できるようにし、レクサーレベルが通常の言語を記述するようにします。これは常に可能とは限りません。
言語を埋め込むとき。一部の言語では、コードを文字列に補間できます"name={expression}"
。式の構文は文脈自由文法の一部であるため、正規表現でトークン化することはできません。これを解決するには、パーサーとレクサーを再結合するか、などの追加のトークンを導入しSTRING-CONTENT, INTERPOLATE-START, INTERPOLATE-END
ます。文字列の文法規則は次のようになりますString → STRING-START STRING-CONTENTS { INTERPOLATE-START Expression INTERPOLATE-END STRING-CONTENTS } STRING-END
。もちろん、式には他の文字列が含まれている可能性があり、次の問題につながります。
トークンがお互いを含むことができる場合。Cライクな言語では、キーワードは識別子と区別できません。これは、識別子よりもキーワードに優先順位を付けることにより、レクサーで解決されます。このような戦略は常に可能とは限りません。Line → IDENTIFIER " = " REST
残りが識別子のように見えても、残りが行末までの任意の文字である構成ファイルを想像してください。例の行は次のようになりますa = b c
。字句解析器は本当に馬鹿げており、トークンがどの順序で発生するかはわかりません。したがって、RESTよりもIDENTIFIERを優先する場合、レクサーはを提供しIDENT(a), " = ", IDENT(b), REST( c)
ます。IDENTIFIERよりもRESTを優先する場合、レクサーは単にを提供しREST(a = b c)
ます。
これを解決するには、レクサーとパーサーを再結合する必要があります。分離は、レクサーをレイジーにすることである程度維持できます。パーサーが次のトークンを必要とするたびに、レクサーからトークンを要求し、受け入れ可能なトークンのセットをレクサーに伝えます。事実上、各位置の字句解析文法の新しい最上位ルールを作成しています。ここでは、これにより呼び出しが発生し、nextToken(IDENT), nextToken(" = "), nextToken(REST)
すべてが正常に機能します。これには、各場所で受け入れ可能なトークンの完全なセットを知っているパーサーが必要です。これは、LRのようなボトムアップパーサーを意味します。
レクサーが状態を維持する必要がある場合。たとえば、Python言語は、中括弧ではなくインデントによってコードブロックを区切ります。文法内でレイアウトに依存する構文を処理する方法はありますが、これらの手法はPythonにとってはやり過ぎです。代わりに、レクサーは各行のインデントをチェックし、新しいインデントされたブロックが見つかった場合はINDENTトークンを、ブロックが終了した場合はDEDENTトークンを発行します。これにより、これらのトークンを中括弧のように見せかけることができるため、メインの文法が単純化されます。ただし、レクサーは現在の状態、つまり現在のインデントを維持する必要があります。これは、レクサーが技術的に通常の言語を記述するのではなく、実際には状況依存言語を記述することを意味します。幸いなことに、この違いは実際には関係がなく、PythonのレクサーはO(n)時間でも動作します。