web-dev-qa-db-ja.com

文法に基づいて字句解析器を作成するときに実行される手順は何ですか?

Clarification about Grammars、Lexers and Parsers の質問に対する回答を読みながら、回答は次のように述べています:

[...] BNF文法には、字句解析と構文解析に必要なすべてのルールが含まれています。

これまで私には少し奇妙なことに遭遇しました。なぜなら、これまでは、字句解析器は文法に基づくものではなくと常に考えていたからです。パーサーは1に大きく基づいていました。字句解析器の記述に関する数多くのブログ投稿を読んだ後、私はこの結論に達しました。 1設計の基礎としてのEBNF/BNF。

レクサーとパーサーがEBNF/BNF文法に基づいている場合、その方法を使用してレクサーを作成するにはどうすればよいでしょうか。つまり、特定のEBNF/BNF文法を使用してレクサーをどのように構築しますか?

私は、EBNF/BNFをガイドまたは青写真として使用してパーサーを作成することを扱ったmany投稿をたくさん見ましたが、何も見つかりませんでしたこれまでのところ、レクサー設計と同等のものを示しています。

たとえば、次の文法を考えてみます。

input = digit| string ;
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"', { all characters - '"' }, '"' ;
all characters = ? all visible characters ? ;

文法に基づいたレクサーをどのように作成しますか?このような文法からパーサーを作成する方法を想像することはできますが、レクサーで同じことを行うという概念を理解することができません。

パーサーの作成など、このようなタスクを実行するために使用される特定のルールまたはロジックはありますか?率直に言って、私は、レクサー設計がEBNF/BNF文法を使用するのか、それともそれに基づいているのか疑問に思い始めています。


1拡張バッカス–ナウル形式 および バッカス–ナウル形式

13
Christian Dean

レクサーは、メインパーサーのパフォーマンス最適化として使用される単純なパーサーです。レクサーがある場合、レクサーとパーサーは連携して完全な言語を記述します。別個の字句解析ステージを持たないパーサーは、「スキャナーレス」と呼ばれることもあります。

レクサーがない場合、パーサーは文字単位で動作する必要があります。パーサーはすべての入力項目に関するメタデータを格納する必要があり、すべての入力項目の状態についてテーブルを事前に計算する必要がある場合があるため、入力サイズが大きい場合、これは許容できないメモリ消費になります。特に、抽象構文ツリーでは、文字ごとに個別のノードは必要ありません。

文字単位のテキストはかなりあいまいであるため、これにより、あいまいさが多くなり、処理が煩わしくなります。ルール_R → identifier | "for " identifier_を想像してみてください。ここでidentifierはASCII文字から構成されています。あいまいさを避けたい場合は、4文字の先読みが必要で、どの代替を選択するかを決定する必要があります。レクサー、パーサーは、IDENTIFIERまたはFORトークン(1トークンの先読み)があるかどうかを確認するだけです。

2レベルの文法。

レクサーは、入力アルファベットをより便利なアルファベットに変換することで機能します。

スキャナーレスパーサーは、文法(N、Σ、P、S)を記述します。非終端記号Nは、文法のルールの左側であり、アルファベットΣは、たとえば、 ASCII=文字。プロダクションPは文法のルールであり、開始記号Sはパーサーの最上位のルールです。

字句解析器は、トークンa、b、c、…のアルファベットを定義します。これにより、メインパーサーはこれらのトークンをアルファベットとして使用できます:Σ= {a、b、c、…}。レクサーの場合、これらのトークンは非終端記号であり、開始規則SL 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)時間。

18
amon