LL解析とLR解析の簡単な例を教えてもらえますか?
大まかに言うと、LL構文解析とLR構文解析の違いは、LL構文解析プログラムは開始記号から開始し、プロダクションを適用してターゲット文字列に到達しようとするのに対して、LR構文解析プログラムはターゲット文字列から開始して開始文字列に戻りますシンボル。
LL解析は、左から右への左端の派生です。つまり、入力シンボルを左から右に検討し、左端の派生を構築しようとします。これは、開始記号から開始し、ターゲット文字列に到達するまで左端の非終端を繰り返し展開することによって行われます。 LR解析は、左から右、右端の派生です。つまり、左から右にスキャンし、右端の派生を構築しようとします。パーサーは、入力のサブストリングを継続的に選択し、それを非端末に逆変換しようとします。
LL解析中、パーサーは次の2つのアクションのいずれかを継続的に選択します。
例として、次の文法を考えます:
int
次に、文字列int + int + int
を指定すると、LL(2)パーサー(lookaheadの2つのトークンを使用)が次のように文字列を解析します。
Production Input Action
---------------------------------------------------------
S int + int + int Predict S -> E
E int + int + int Predict E -> T + E
T + E int + int + int Predict T -> int
int + E int + int + int Match int
+ E + int + int Match +
E int + int Predict E -> T + E
T + E int + int Predict T -> int
int + E int + int Match int
+ E + int Match +
E int Predict E -> T
T int Predict T -> int
int int Match int
Accept
各ステップで、プロダクションの左端のシンボルを見ることに注意してください。それが端末である場合、それを照合し、それが非端末である場合、ルールの1つを選択することにより、それがどうなるかを予測します。
LRパーサーには、2つのアクションがあります。
例として、LR(1)パーサー(lookaheadの1つのトークンを使用)は、次のように同じ文字列を解析できます。
Workspace Input Action
---------------------------------------------------------
int + int + int Shift
int + int + int Reduce T -> int
T + int + int Shift
T + int + int Shift
T + int + int Reduce T -> int
T + T + int Shift
T + T + int Shift
T + T + int Reduce T -> int
T + T + T Reduce E -> T
T + T + E Reduce E -> T + E
T + E Reduce E -> T + E
E Reduce S -> E
S Accept
あなたが言及した2つの解析アルゴリズム(LLおよびLR)は、異なる特性を持つことが知られています。 LLパーサーは手書きで書くのが簡単な傾向がありますが、LRパーサーよりも強力ではなく、LRパーサーよりもはるかに小さい文法セットを受け入れます。 LRパーサーには多くのフレーバー(LR(0)、SLR(1)、LALR(1)、LR(1)、IELR(1)、GLR(0)など)があり、はるかに強力です。また、より複雑になる傾向があり、ほとんどの場合、yacc
やbison
などのツールによって生成されます。 LLパーサーにも多くのフレーバーがあります( ANTLR
ツールで使用されるLL(*)を含む)が、実際にはLL(1)が最も広く使用されています。
恥知らずのプラグインとして、LLおよびLR解析の詳細を知りたい場合は、コンパイラーコースの指導を終えたばかりで、コースのWebサイトに 解析に関するいくつかの配布資料と講義スライド があります。役に立つと思われる場合は、それらのいずれかについて詳しく説明させていただきます。
Josh Habermanの記事 LLおよびLR Parsing Demystified は、LL解析が ポーランド表記法 に直接対応するのに対し、LRは 逆ポーランド表記法 に対応すると主張しています。 PNとRPNの違いは、方程式の二分木を走査する順序です。
+ 1 * 2 3 // Polish (prefix) expression; pre-order traversal.
1 2 3 * + // Reverse Polish (postfix) expression; post-order traversal.
Habermanによると、これはLLパーサーとLRパーサーの主な違いを示しています。
LLパーサーとLRパーサーの動作方法の主な違いは、LLパーサーが解析ツリーの事前順序トラバーサルを出力し、LRパーサーが事後順序トラバーサルを出力することです。
詳細な説明、例、結論については、ハーバーマンの 記事 をご覧ください。
LLはトップダウンを使用し、LRはボトムアップアプローチを使用します。
プログラム言語を解析する場合:
LRと比較した場合、LL解析は障害があります。 LLパーサージェネレーターにとって悪夢のような文法を次に示します。
Goal -> (FunctionDef | FunctionDecl)* <eof>
FunctionDef -> TypeSpec FuncName '(' [Arg/','+] ')' '{' '}'
FunctionDecl -> TypeSpec FuncName '(' [Arg/','+] ')' ';'
TypeSpec -> int
-> char '*' '*'
-> long
-> short
FuncName -> IDENTIFIER
Arg -> TypeSpec ArgName
ArgName -> IDENTIFIER
FunctionDefは、 ';'まではFunctionDeclとまったく同じように見えます。または「{」が検出されます。
LLパーサーは同時に2つのルールを処理できないため、FunctionDefまたはFunctionDeclを選択する必要があります。しかし、どちらが正しいかを知るには、「;」を先読みする必要がありますまたは '{'。文法分析時には、先読み(k)は無限に見えます。解析時には有限ですが、大きくなる可能性があります。
LRパーサーは、2つのルールを同時に処理できるため、先読みする必要はありません。したがって、LALR(1)パーサージェネレーターはこの文法を簡単に処理できます。
入力コードが与えられた場合:
int main (int na, char** arg);
int main (int na, char** arg)
{
}
LRパーサーは、
int main (int na, char** arg)
「;」に遭遇するまで、どのルールが認識されているかを気にせずにまたは「{」。
LLパーサーは、どの規則が認識されているかを知る必要があるため、「int」でハングアップします。したがって、「;」を先読みする必要があります。または '{'。
LLパーサーのもう1つの悪夢は、文法の再帰です。左再帰は文法では普通のことであり、LRパーサージェネレーターには問題ありませんが、LLはそれを処理できません。
そのため、LLを使用して不自然な方法で文法を作成する必要があります。
左端の派生例:文脈自由な文法Gにはプロダクションがあります
z→xXY(ルール:1)X→Ybx(ルール:2)Y→bY(ルール:3)Y→c(ルール:4)
文字列w = 'xcbxbc'を左端の派生で計算します。
z⇒xXY(ルール:1)⇒xYbxY(ルール:2)⇒xcbxY(ルール:4)⇒xcbxbY(ルール:3)⇒xcbxbc(ルール:4)
右端の派生例: K→aKK(ルール:1)A→b(ルール:2)
文字列w = 'aababbb'を右端の派生で計算します。
K⇒aKK(ルール:1)⇒aKb(ルール:2)⇒aaKKb(ルール:1)⇒aaKaKKb(ルール:1)⇒aaKaKbb(ルール:2)⇒aaKabbb(ルール:2)⇒aababbb(ルール:2)