これらの別々の技術が存在する理由と、それらの長所と短所が何であるかをよりよく理解するために、私はこの数日、多くの研究を行いました。
すでに存在する回答のいくつかはそれらの違いの一部を示唆していましたが、完全な状況を示しておらず、やや意見が多いように見えたため、この回答が書かれました。
この説明は長いですが、重要です。我慢してください(または焦りを感じている場合は、最後までスクロールしてフローチャートを表示してください)。
パーサーコンビネーターとパーサージェネレーターの違いを理解するには、まず、存在するさまざまな種類の解析の違いを理解する必要があります。
解析
構文解析は、正式な文法に従って一連の記号を分析するプロセスです。(コンピューティングサイエンスでは)解析を使用して、コンピューターに言語で書かれたテキストを理解させることができます。通常、書かれたテキストを表す解析ツリーを作成し、ツリーの各ノードのさまざまな書かれた部分の意味を保存します。この解析ツリーは、さまざまな目的に使用できます(多くのコンパイラで使用される)別の言語への翻訳、何らかの方法(SQL、HTML)で書かれた命令の直接解釈、Lintersなどのツールでの
作業などなど。解析ツリーは明示的に生成されますが、ツリー内の各タイプのノードで実行されるアクションが直接実行されます。これにより効率が向上しますが、水中では暗黙的な解析ツリーが依然として存在します。
解析は計算上困難な問題です。このテーマに関する研究は50年以上ありましたが、まだ学ぶべきことがたくさんあります。
大まかに言えば、コンピューターが入力を解析できるようにする4つの一般的なアルゴリズムがあります。
- LL解析。(コンテキストフリーのトップダウン解析。)
- LR解析。(コンテキストフリーのボトムアップ解析。)
- PEG + Packrat解析。
- アーリー解析。
これらのタイプの解析は、非常に一般的な理論的な説明であることに注意してください。これらの各アルゴリズムを物理マシンに実装するには、トレードオフが異なる複数の方法があります。
LLとLRは、Context-Free文法のみを見ることができます(つまり、書き込まれたトークンの周囲のコンテキストは、それらの使用方法を理解するために重要ではありません)。
PEG / Packrat構文解析とEarley構文解析はあまり使用されません。Earley構文解析は、より多くの文法(必ずしもContext-Freeである必要のない文法を含む)を処理できるという点で優れていますが、(ドラゴンが主張するように)本(セクション4.1.1);これらの主張がまだ正確かどうかはわかりません)。
式の構文解析+ Packrat構文解析は、比較的効率的で、LLとLRの両方よりも多くの文法を処理できる方法ですが、以下で簡単に説明するように、あいまいさを隠します。
LL(左から右、左端の派生)
これは、おそらく解析について考える最も自然な方法です。入力文字列の次のトークンを見て、複数の可能な再帰呼び出しのどれをツリー構造を生成するために取るかを決定するという考え方です。
このツリーは「トップダウン」で構築されます。つまり、ツリーのルートから開始し、入力文字列を移動するのと同じ方法で文法規則を移動します。また、読み取られている「infix」トークンストリームに相当する「postfix」を構築していると見なすこともできます。
LLスタイルの解析を実行するパーサーは、指定された元の文法に非常によく似たように記述できます。これにより、比較的簡単に理解、デバッグ、および強化できます。クラシックパーサーコンビネーターは、LLスタイルパーサーを構築するためにまとめることができる「レゴピース」にすぎません。
LR(左から右、右端の派生)
LR解析は逆の方法でボトムアップで行われます。各ステップで、スタックの最上位の要素が文法のリストと比較され、それらが
文法の上位レベルのルールに縮小できるかどうかが確認されます。そうでない場合、入力ストリームからの次のトークンはシフトされ、スタックの一番上に配置されます。
最後に、文法の開始規則を表す単一のノードがスタック上にある場合、プログラムは正しいです。
先のことを考える
これら2つのシステムのいずれかでは、選択を決定する前に、入力からさらにトークンを覗く必要がある場合があります。これは(0)
、(1)
、(k)
または(*)
次のようなこれら二つの一般的なアルゴリズムの名前の後に表示さ-syntax LR(1)
かLL(k)
。k
通常は「あなたの文法が必要とするだけ」を*
表し、通常「このパーサーはバックトラッキングを実行します」を表します。直線的に。
LRスタイルのパーサーは、「先読み」することを決定するかもしれないときに、スタック上にすでに多くのトークンを持っているので、ディスパッチする情報が既にあることに注意してください。これは、同じ文法の場合、LLスタイルのパーサーよりも「先読み」の必要性が少ないことを意味します。
LL vs. LR:アンビゲティ
上記の2つの説明を読むと、LLスタイルの構文解析がより自然に思えるので、なぜLRスタイルの構文解析が存在するのか疑問に思うかもしれません。
ただし、LLスタイルの解析には問題があります:Left Recursion。
次のような文法を書くことは非常に自然です。
expr ::= expr '+' expr | term
term ::= integer | float
ただし、LLスタイルのパーサーは、この文法を解析するときに無限再帰ループにexpr
陥ります。ルールの左端の可能性を試すと、入力を消費せずにこのルールに再帰します。
この問題を解決する方法があります。最も簡単なのは、この種の再帰が発生しないように文法を書き直すことです。
expr ::= term expr_rest
expr_rest ::= '+' expr | ϵ
term ::= integer | float
(ここで、εは、「空の文字列」の略)
この文法は今や再帰的です。すぐに読むのがずっと難しくなることに注意してください。
実際には、左再帰は他の多くのステップを介して間接的に発生する可能性があります。これは、注意するのが難しい問題です。しかし、それを解決しようとすると、文法が読みにくくなります。
Dragon Bookのセクション2.5には次のように記載されています。
矛盾があるように見えます。一方で翻訳を容易にする文法が必要であり、他方で構文解析を容易にする著しく異なる文法が必要です。解決策は、簡単に翻訳できるように文法から始めて、構文解析を容易にするために慎重に変換することです。左再帰を排除することにより、予測再帰下降トランスレーターでの使用に適した文法を取得できます。
LRスタイルのパーサーは、ツリーをボトムアップで構築するため、この左再帰の問題はありません。
しかし、(多くの場合として実装されているLRスタイルのパーサに上記のような文法の精神的な翻訳有限状態オートマトンは)
+(およびエラーが発生しやすいが)のように、多くの場合、状態の数百または数千がありますが、やることは非常に困難です考慮すべき状態遷移。これが、LRスタイルのパーサーが通常「コンパイラコンパイラ」とも呼ばれるパーサージェネレータによって生成される理由です。
あいまいさを解決する方法
上記の左再帰のあいまいさを解決する2つの方法を見ました:1)構文を書き直します2)LRパーサーを使用します。
しかし、解決が難しい他の種類のあいまいさがあります。2つの異なるルールが同時に等しく適用できる場合はどうでしょうか。
一般的な例を次に示します。
LLスタイルとLRスタイルの両方のパーサーには、これらの問題があります。算術式の解析に関する問題は、演算子の優先順位を導入することで解決できます。同様に、Dangling Elseのような他の問題は、1つの優先動作を選択し、それに固執することで解決できます。(たとえば、C / C ++では、宙ぶらりんのelseは常に最も近い 'if'に属します)。
これに対する別の「解決策」は、Parser Expression Grammar(PEG)を使用することです。これは、上記で使用したBNF文法に似ていますが、あいまいな場合は、常に「最初を選択」します。もちろん、これは実際に問題を「解決」するのではなく、むしろ曖昧さが実際に存在することを隠しています。
文法にあいまいさがないかどうかを一般に知ることが不可能である理由や、この意味合いが素晴らしいブログ記事LLとLRのコンテキストであるなど、この投稿よりもはるかに詳細な詳細情報ツールは難しいです。強くお勧めできます。今話しているすべてのことを理解するのに大いに役立ちました。
50年の研究
それでも人生は続く。有限状態オートマトンとして実装された「通常の」LRスタイルのパーサーは、数千の状態と遷移を必要とすることが多く、これはプログラムサイズの問題でした。そのため、Simple LR(SLR)やLALR(Look-ahead LR)などのバリアントが記述され、他の手法を組み合わせてオートマトンを小さくし、パーサープログラムのディスクとメモリのフットプリントを削減しました。
また、上記のあいまいさを解決する別の方法は、あいまいさの場合、両方の可能性を保持および解析する一般化された手法を使用することです:どちらかが行の解析に失敗する可能性があります'正しい')、および両方が正しい場合に両方を返す(この方法であいまいさが存在することを示す)。
興味深いことに、一般化LRアルゴリズムについて説明した後、同様のアプローチを使用して一般化LLパーサーを実装できることがわかりました。これは、同様に高速です(曖昧な文法の$ O(n ^ 3)$時間の複雑さ、$ O(n)単純な(LA)LRパーサーよりもブックキーピングは多いものの、完全に明確な文法のための$。これは、より高い定数係数を意味しますが、より自然な再帰降下(トップダウン)スタイルでパーサーを記述できるようにします。記述してデバッグします。
パーサーコンビネーター、パーサージェネレーター
したがって、この長い博覧会で、私たちは今、問題の核心に到達しています:
パーサーコンビネーターとパーサージェネレーターの違いは何ですか?
彼らは本当に異なる種類の獣です:
パーサーコンビネーターが作成されたのは、人々がトップダウンパーサーを書いていて、これらの多くに共通点が多いことに気づいたからです。
パーサージェネレーターが作成されたのは、LLスタイルのパーサーが抱えていた問題のないパーサー(つまり、LRスタイルのパーサー)を構築しようとしていたためです。一般的なものには、(LA)LRを実装するYacc / Bisonが含まれます。
おもしろいことに、今日では景色が多少混乱しています。
GLLアルゴリズムで動作するパーサーコンビネーターを記述して、古典的なLLスタイルパーサーが抱えていた曖昧さの問題を解決しながら、あらゆる種類のトップダウンパーシングと同じように読み取り/理解することができます。
パーサージェネレーターは、LLスタイルのパーサー用に作成することもできます。ANTLRはそれを正確に行い、他のヒューリスティック(Adaptive LL(*))を使用して、古典的なLLスタイルのパーサーが持つあいまいさを解決します。
一般に、LRパーサージェネレーターを作成し、グラマーで実行されている(LA)LRスタイルのパーサージェネレーターの出力をデバッグすることは、元のグラマーを「inside-out」LRフォームに変換するため、困難です。一方、Yacc / Bisonのようなツールは長年にわたって最適化されており、多くの人が実際に使用されています。つまり、多くの人が構文解析を行う方法と見なしており、新しいアプローチに懐疑的です。
どちらを使用すべきかは、文法の難易度と、パーサーの速度に依存します。文法に応じて、これらの手法の1つ(さまざまな手法の実装)は、他の手法よりも高速であるか、メモリフットプリントが小さいか、ディスクフットプリントが小さいか、拡張性が高いか、デバッグが容易です。マイレージは異なる場合があります。
サイドノート:字句解析について。
字句解析は、パーサーコンビネーターとパーサージェネレーターの両方に使用できます。アイデアは、実装が非常に簡単(したがって高速)の「ダム」パーサーを使用して、ソースコードの最初のパスを実行し、たとえば繰り返しの空白、コメントなどを削除し、場合によっては「トークン化」することですあなたの言語を構成するさまざまな要素の大まかな方法。
主な利点は、この最初のステップにより、実際のパーサーが非常に単純になることです(そのため、おそらくより高速になります)。主な欠点は、別の変換手順があることです。たとえば、空白の削除により、行番号と列番号のエラー報告が難しくなります。
最後のレクサーは「単なる」別のパーサーであり、上記の手法のいずれかを使用して実装できます。その単純さのため、多くの場合、メインパーサー以外の他の手法が使用され、たとえば追加の「レクサージェネレーター」が存在します。
Tl; Dr:
ほとんどの場合に適用できるフローチャートを次に示します。
javac
、Scalaなど)の実装の好ましい形式です。それはあなたに良いエラーメッセージを生成するのに役立ちます内部パーサ状態、(近年では...で最も制御できます