文法に基づいてレクサーを作成するときに従う手順は何ですか?


12

文法、レクサー、パーサーに関する質問Clarificationに対する回答を読んでいると、答えは次のように述べています。

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

パーサーは文法に基づいているのに対し、これまでは常に字句解析は文法に基づいていないと考えていたため、これはやや奇妙に思えました。レクサーの作成に関する多数のブログ投稿を読んだ後、この結論に至りました。デザインの基礎として1つのEBNF / BNF を使用したことはありません。

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

EBNF / BNFをガイドまたは設計図として使用してパーサーを記述することを扱った多くの投稿を見てきましたが、レクサーデザインと同等のことを示すものは今のところ見つかりませんでした。

たとえば、次の文法を取ります。

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 拡張バッカス-ナウア形式バッカス-ナウア形式

回答:


17

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

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

文字ごとのテキストはかなりあいまいであるため、これはまた、処理するのが面倒なはるかにあいまいな結果になります。ルールを想像してください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)時間で認識できます。実際には、コードジェネレーターはそのようなレクサーを非常に効率的なジャンプテーブルに変えることができます。

文法からトークンを抽出します。

あなたの例に触れるには:digitstringルールは文字ごとのレベルで表現されます。それらをトークンとして使用できます。残りの文法はそのままです。以下は、正規であることを明確にするために右線形文法として書かれた字句解析文法です。

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)時間でも動作します。


@amonの非常に良い答え、ありがとう。私はそれを完全に消化するのに時間がかかるでしょう。しかし、あなたの答えについていくつか疑問に思っていました。8番目の段落の前後で、EBNF文法の例をパーサーのルールに変更する方法を示します。示した文法はパーサーでも使用されますか?または、パーサー用の別の文法がまだありますか?
クリスチャンディーン

@Engineerいくつかの編集を行いました。EBNFはパーサーで直接使用できます。ただし、私の例では、文法のどの部分を別のレクサーで処理できるかを示しています。他のルールはすべてメインパーサーによって処理されますが、この例ではそれだけinput = digit | stringです。
アモン

4
スキャナーレスパーサーの大きな利点は、作成がはるかに簡単なことです。その極端な例では何も行いませんパーサコンビネータライブラリ、ですが、コンパーサを。構文解析器の作成は、ECMAScriptに埋め込まれたHTMLに埋め込まれたPHPが散りばめられたSQLにテンプレート言語が付いている、またはRuby-examplesが埋め込まれたマークダウンなどの場合に興味深いRuby-documentation-commentsの埋め込みなど。
ヨルグWミットタグ

最後の箇条書きは非常に重要ですが、あなたが書いた方法は誤解を招くと感じています。インデントベースの構文ではレクサーを簡単に使用できないのは事実ですが、その場合、スキャナーなしの解析はさらに難しくなります。実際には、そうしたいあなたは言語のようなものを持っている場合は、関連する状態でそれを増強、レクサーを使用します。
user541686

@Mehrdad Pythonスタイルのレクサー駆動のインデント/デデントトークンは、インデントに敏感な非常に単純な言語でのみ使用でき、一般的には適用できません。より一般的な代替手段は属性文法ですが、標準ツールにはそれらのサポートがありません。アイデアは、すべてのASTフラグメントにインデントを付けて注釈を付け、すべてのルールに制約を追加することです。属性は、コンビネーター解析を使用して簡単に追加できます。これにより、スキャナーなしの解析も容易になります。
アモン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.