108

LR、SLR、および LALR パーサーの実際の違いは何ですか? SLR と LALR が LR パーサーの一種であることは知っていますが、解析テーブルに関する限り、実際の違いは何ですか?

また、文法が LR、SLR、LALR のいずれであるかを示すにはどうすればよいでしょうか? LL 文法の場合、解析テーブルのどのセルにも複数の生成規則が含まれてはならないことを示す必要があります。LALR、SLR、および LR の同様のルールはありますか?

たとえば、文法が

S --> Aa | bAc | dc | bda
A --> d

LALR(1) ですが、SLR(1) ではありませんか?


EDIT(ybungalobill):LALRとLRの違いについて満足のいく答えが得られませんでした。そのため、LALR のテーブルはサイズが小さくなりますが、LR 文法のサブセットしか認識できません。LALRとLRの違いについて誰か詳しく教えてください。答えには LALR(1) と LR(1) で十分です。どちらも 1 トークンの先読みを使用し、どちらもテーブル駆動型です! それらはどのように違うのですか?

4

9 に答える 9

70

SLR、LALR、および LR パーサーはすべて、まったく同じテーブル駆動機構を使用して実装できます。

基本的に、解析アルゴリズムは次の入力トークン T を収集し、現在の状態 S (および関連する先読み、GOTO、リダクション テーブル) を調べて、何をすべきかを決定します。

  • SHIFT: 現在のテーブルがトークン T の SHIFT を指示している場合、ペア (S,T) が解析スタックにプッシュされ、現在のトークンに対する GOTO テーブルの指示に従って状態が変更されます (例: GOTO(T) )、別の入力トークン T' がフェッチされ、プロセスが繰り返されます
  • REDUCE: すべての状態には、その状態で発生する可能性のある 0、1、または多数の可能な削減があります。パーサーが LR または LALR の場合、トークンは、状態のすべての有効な縮小について、先読みセットに対してチェックされます。トークンが文法規則 G = R1 R2 .. Rn のリダクションの先読みセットと一致する場合、スタックのリダクションとシフトが発生します。G のセマンティック アクションが呼び出され、スタックが (Rn から) n 回ポップされ、ペア ( S,G) がスタックにプッシュされ、新しい状態 S' が GOTO(G) に設定され、同じトークン T でサイクルが繰り返されます。パーサーが SLR パーサーである場合、状態であるため、どの削減が適用されるかを検索することなく、やみくもに削減アクションを実行できます。SLR パーサーが存在するかどうかを知ると便利です削減かどうか; これは、各状態がそれに関連付けられた削減の数を明示的に記録しているかどうかを簡単に判断できます。実際には、L(AL)R バージョンにはその数が必要です。
  • エラー: SHIFT も REDUCE も使用できない場合は、構文エラーが宣言されます。

では、それらがすべて同じ機械を使用している場合、何の意味があるのでしょうか?

SLR の価値は、その実装の単純さにあります。先読みセットは多くても 1 つしかないため、考えられるリダクションをチェックするためにスキャンする必要はありません。SHIFT で状態を終了しない場合は、これが唯一の実行可能なアクションです。どのリダクションを適用するかは、状態に具体的に関連付けることができるため、SLR 解析機構はそれを探す必要がありません。実際には、L(AL)R パーサーは非常に多くの言語セットを扱い、実装するのに余分な作業がほとんどないため、学術的な演習としてのみ SLR を実装する人はいません。

LALR と LR の違いは、テーブルジェネレーターに関係しています。. LR パーサー ジェネレーターは、特定の状態とその正確な先読みセットから可能なすべての削減を追跡します。すべてのリダクションが左のコンテキストからの正確な先読みセットに関連付けられている状態になります。これは、かなり大きな状態のセットを構築する傾向があります。LALR パーサー ジェネレーターは、リダクション用の GOTO テーブルとルックヘッド セットに互換性があり、競合しない場合、状態を結合します。これは、LR が区別できる特定のシンボル シーケンスを区別できないという代償を払って、かなり少数の状態を生成します。そのため、LR パーサーは LALR パーサーよりも多くの言語セットを解析できますが、非常に大きなパーサー テーブルを持ちます。実際には、ステートマシンのサイズを最適化する価値があるターゲット言語に十分近い LALR 文法を見つけることができます。

だから:3つすべてが同じ機械を使用しています。SLR は、ほんの少しの機械を無視できるという意味では「簡単」ですが、手間をかける価値はありません。LR はより広範な言語セットを解析しますが、状態テーブルはかなり大きくなる傾向があります。そのため、LALR が実用的な選択肢になります。

以上のことをすべて述べた上で、GLR パーサーは、より複雑な機構を使用しながら、まったく同じテーブル(LALR で使用されるより小さなバージョンを含む) を使用して、任意の文脈自由言語を解析できることを知っておく価値があります。これは、GLR が LR、LALR、SLR より厳密に強力であることを意味します。標準的な BNF 文法を書くことができれば、GLR はそれに従って構文解析します。機構の違いは、GOTO テーブルと先読みセットの間に競合がある場合、GLR が複数の解析を試行することです。(GLR がこれを効率的に行う方法は [私のものではない] 天才ですが、この SO の投稿には収まりません)。

それは私にとって非常に有益な事実です。私はプログラム アナライザーとコード トランスフォーマーとパーサーを構築しますが、必要ですが「面白くない」です。興味深い作業は、解析された結果に対して何を行うかであり、解析後の作業に焦点が当てられます。GLR を使用すると、文法をハッキングして LALR で使用可能な形式にするよりも、実用的な文法を比較的簡単に構築できます。これは、C++ や Fortran などの非アカデミック言語に対処しようとする場合に非常に重要です。言語全体を適切に処理するには、文字通り何千もの規則が必要であり、文法規則をハッキングしようとして人生を費やしたくありません。 LALR (または LR) の制限を満たします。

一種の有名な例として、C++ は解析が非常に難しいと考えられています... LALR 解析を行っている人たちによって。C++ は、C++ リファレンス マニュアルの最後に記載されているほとんどのルールを使用して、GLR 機構を使用して簡単に解析できます。(私はまさにそのようなパーサーを持っており、バニラ C++ だけでなく、さまざまなベンダーの方言も処理します。GLR パーサー、IMHO を使用しているため、これは実際にのみ可能です)。

[2011 年 11 月編集: すべての C++11 を処理できるようにパーサーを拡張しました。GLR を使用すると、それがはるかに簡単になりました。EDIT 2014 年 8 月: C++17 のすべてを処理するようになりました。壊れたり悪くなったりしたことはありません.GLRはまだ猫の鳴き声です.]

于 2010-10-22T04:40:24.820 に答える
19

LALR パーサーは、LR 文法内の同様の状態をマージして、同等の SLR 文法とまったく同じサイズのパーサー状態テーブルを生成します。これは、通常、純粋な LR 解析テーブルよりも一桁小さくなります。ただし、複雑すぎて LALR にできない LR 文法の場合、これらのマージされた状態によってパーサーの競合が発生するか、元の LR 文法を完全には認識しないパーサーが生成されます。

ところで、MLR(k) 解析テーブル アルゴリズムhere でこれについていくつか言及します。

補遺

簡単に言えば、LALR 解析テーブルは小さくなりますが、パーサー機構は同じです。すべての LR 状態が生成され、多くの冗長な (ほぼ同一の) 状態がある場合、与えられた LALR 文法ははるかに大きな解析テーブルを生成します。

同様の (冗長な) 状態がマージされ、個別の状態がエンコードするコンテキスト/先読み情報を効果的に破棄するため、LALR テーブルは小さくなります。利点は、同じ文法に対して非常に小さな解析テーブルを取得できることです。

欠点は、すべての LR 文法を LALR テーブルとしてエンコードできるわけではないことです。より複雑な文法にはより複雑な先読みがあり、1 つのマージされた状態ではなく 2 つ以上の状態になるためです。

主な違いは、LR テーブルを生成するアルゴリズムは、状態から状態への遷移の間により多くの情報を運ぶのに対し、LALR アルゴリズムはそうではないことです。そのため、LALR アルゴリズムは、特定のマージされた状態を本当に 2 つ以上の別個の状態として残す必要があるかどうかを判断できません。

于 2010-10-22T21:16:34.010 に答える
12

さらに別の答え(YAA)。

SLR(1)、LALR(1)、および LR(1) の解析アルゴリズムは、Ira Baxter が言ったように同じです
が、パーサー生成アルゴリズムのためにパーサー テーブルが異なる場合があります。

SLR パーサー ジェネレーターは、LR(0) ステート マシンを作成し、文法 (FIRST および FOLLOW セット) から先読みを計算します。これは単純化されたアプローチであり、LR(0) ステート マシンには実際には存在しない競合が報告される場合があります。

LALR パーサー ジェネレーターは、LR(0) ステート マシンを作成し、LR(0) ステート マシンから先読みを計算します (端末遷移を介して)。これは正しいアプローチですが、LR(1) ステート マシンには存在しない競合が報告されることがあります。

Canonical LR パーサー ジェネレーターは LR(1) ステート マシンを計算し、先読みは既に LR(1) ステート マシンの一部です。これらのパーサー テーブルは非常に大きくなる可能性があります。

最小 LR パーサー ジェネレーターは LR(1) ステート マシンを計算しますが、プロセス中に互換性のある状態をマージしてから、最小 LR(1) ステート マシンから先読みを計算します。これらのパーサー テーブルは、LALR パーサー テーブルと同じかわずかに大きいサイズであり、最適なソリューションを提供します。

LRSTAR 10.0は、文法に必要なものは何でも、C++ で LALR(1)、LR(1)、CLR(1)、または LR(*) パーサーを生成できます。LR パーサー間の違いを示すこの図を参照

[完全な開示: LRSTAR は私の製品です]

于 2013-05-15T21:24:06.850 に答える
5

SLR と LR で生成されたパーサー テーブルの基本的な違いは、reduce アクションが SLR テーブルの Follows セットに基づいていることです。これは過度に制限される可能性があり、最終的には shift-reduce 競合が発生します。

一方、LR パーサーは、縮小される非終端記号を実際に追跡できる終端集合のみに基づいて縮小決定を行います。このターミナルのセットは、多くの場合、そのような非ターミナルのフォローセットの適切なサブセットであるため、シフトアクションと競合する可能性は低くなります。

このため、LR パーサーはより強力です。ただし、LR 解析テーブルは非常に大きくなる可能性があります。

LALR パーサーは、LR 解析テーブルを構築するというアイデアから始まりますが、生成された状態を組み合わせてテーブル サイズを大幅に小さくします。欠点は、LR テーブルを使用しないと回避できた一部の文法で、競合が発生する可能性がわずかに生じることです。

LALR パーサーは、LR パーサーよりもわずかに強力ではありませんが、SLR パーサーよりも強力です。YACC などのパーサー ジェネレーターは、この理由から LALR を使用する傾向があります。

PS 簡潔にするために、上記の SLR、LALR、および LR は実際には SLR(1)、LALR(1)、および LR(1) を意味するため、1 つのトークンの先読みが暗示されます。

于 2011-08-30T03:13:44.627 に答える
0

上記の回答に加えて、ボトムアップ LR パーサーのクラスの個々のパーサー間の違いは、解析テーブルを生成するときにシフト/リデュースまたはリデュース/リデュースの競合が発生するかどうかです。競合が少ないほど、文法はより強力になります (LR(0) < SLR(1) < LALR(1) < CLR(1))。

たとえば、次の式の文法を考えてみましょう。

E → E + T

え→た

T→F

T → T * F

へ → ( へ )

F→id

LR(0) ではなく SLR(1) です。次のコードを使用して、LR0 オートマトンを構築し、解析テーブルを構築できます (文法を拡張し、クロージャを使用して DFA を計算し、アクションを計算してセットに移動する必要があります)。

from copy import deepcopy
import pandas as pd

def update_items(I, C):
    if len(I) == 0:
         return C
    for nt in C:
         Int = I.get(nt, [])
         for r in C.get(nt, []):
              if not r in Int:
                  Int.append(r)
          I[nt] = Int
     return I

def compute_action_goto(I, I0, sym, NTs): 
    #I0 = deepcopy(I0)
    I1 = {}
    for NT in I:
        C = {}
        for r in I[NT]:
            r = r.copy()
            ix = r.index('.')
            #if ix == len(r)-1: # reduce step
            if ix >= len(r)-1 or r[ix+1] != sym:
                continue
            r[ix:ix+2] = r[ix:ix+2][::-1]    # read the next symbol sym
            C = compute_closure(r, I0, NTs)
            cnt = C.get(NT, [])
            if not r in cnt:
                cnt.append(r)
            C[NT] = cnt
        I1 = update_items(I1, C)
    return I1

def construct_LR0_automaton(G, NTs, Ts):
    I0 = get_start_state(G, NTs, Ts)
    I = deepcopy(I0)
    queue = [0]
    states2items = {0: I}
    items2states = {str(to_str(I)):0}
    parse_table = {}
    cur = 0
    while len(queue) > 0:
        id = queue.pop(0)
        I = states[id]
        # compute goto set for non-terminals
        for NT in NTs:
            I1 = compute_action_goto(I, I0, NT, NTs) 
            if len(I1) > 0:
                state = str(to_str(I1))
                if not state in statess:
                    cur += 1
                    queue.append(cur)
                    states2items[cur] = I1
                    items2states[state] = cur
                    parse_table[id, NT] = cur
                else:
                    parse_table[id, NT] = items2states[state]
        # compute actions for terminals similarly
        # ... ... ...
                    
    return states2items, items2states, parse_table
        
states, statess, parse_table = construct_LR0_automaton(G, NTs, Ts)

ここで、文法 G、非終端記号および終端記号は次のように定義されます

G = {}
NTs = ['E', 'T', 'F']
Ts = {'+', '*', '(', ')', 'id'}
G['E'] = [['E', '+', 'T'], ['T']]
G['T'] = [['T', '*', 'F'], ['F']]
G['F'] = [['(', 'E', ')'], ['id']]

上記の LR(0) 解析テーブル生成用に実装した便利な関数を次にいくつか示します。

def augment(G, S): # start symbol S
    G[S + '1'] = [[S, '$']]
    NTs.append(S + '1')
    return G, NTs

def compute_closure(r, G, NTs):
    S = {}
    queue = [r]
    seen = []
    while len(queue) > 0:
        r = queue.pop(0)
        seen.append(r)
        ix = r.index('.') + 1
        if ix < len(r) and r[ix] in NTs:
            S[r[ix]] = G[r[ix]]
            for rr in G[r[ix]]:
                if not rr in seen:
                    queue.append(rr)
    return S

次の図 (展開して表示) は、上記のコードを使用して文法用に構築された LR0 DFA を示しています。

ここに画像の説明を入力

次の表は、pandas データフレームとして生成された LR(0) 解析テーブルを示しています。文法が LR(0) ではないことを示すシフト/リデュースの競合がいくつかあることに注意してください。

ここに画像の説明を入力

SLR(1) パーサーは、次の入力トークンが還元される非終端記号のフォロー セットのメンバーである場合にのみ還元することで、上記のシフト/還元の競合を回避します。次の解析テーブルは、SLR によって生成されます。

ここに画像の説明を入力

次のアニメーションは、入力式が上記の SLR(1) 文法によってどのように解析されるかを示しています。

ここに画像の説明を入力

質問の文法も LR(0) ではありません。

#S --> Aa | bAc | dc | bda
#A --> d    
G = {}
NTs = ['S', 'A']
Ts = {'a', 'b', 'c', 'd'}
G['S'] = [['A', 'a'], ['b', 'A', 'c'], ['d', 'c'], ['b', 'd', 'a']]
G['A'] = [['d']]

次の LR0 DFA と解析テーブルからわかるように:

ここに画像の説明を入力

shift / reduce の競合が再び発生しています。

ここに画像の説明を入力

ただし、フォームの文字列を受け入れる次の文法a^ncb^n, n >= 1は LR(0) です。

あ → あ あ b

あ→あ

し→あ

# S --> A 
# A --> a A b | c
G = {}
NTs = ['S', 'A']
Ts = {'a', 'b', 'c'}
G['S'] = [['A']]
G['A'] = [['a', 'A', 'b'], ['c']]

ここに画像の説明を入力

次の図からわかるように、生成された解析テーブルには競合がありません。

ここに画像の説明を入力

a^2cb^2次のコードを使用して、上記の LR(0) 解析テーブルを使用して入力文字列を解析する方法を次に示します。

def parse(input, parse_table, rules):
    input = 'aaacbbb$'
    stack = [0]
    df = pd.DataFrame(columns=['stack', 'input', 'action'])
    i, accepted = 0, False
    while i < len(input):
        state = stack[-1]
        char = input[i]
        action = parse_table.loc[parse_table.states == state, char].values[0]
        if action[0] == 's':   # shift
            stack.append(char)
            stack.append(int(action[-1]))
            i += 1
        elif action[0] == 'r': # reduce
            r = rules[int(action[-1])]
            l, r = r['l'], r['r']
            char = ''
            for j in range(2*len(r)):
                s = stack.pop()
                if type(s) != int:
                    char = s + char
            if char == r:
                goto = parse_table.loc[parse_table.states == stack[-1], l].values[0]
                stack.append(l)
                stack.append(int(goto[-1]))
        elif action == 'acc':  # accept
            accepted = True
        df2 = {'stack': ''.join(map(str, stack)), 'input': input[i:], 'action': action}
        df = df.append(df2, ignore_index = True)
        if accepted:
            break
        
    return df

parse(input, parse_table, rules)

次のアニメーションはa^2cb^2、上記のコードを使用して入力文字列が LR(0) パーサーで解析される方法を示しています。

ここに画像の説明を入力

于 2021-11-30T23:02:19.150 に答える
-3

簡単な答えの 1 つは、すべての LR(1) 文法は LALR(1) 文法であるということです。LALR(1) と比較して、LR(1) は関連する有限状態マシンにより多くの状態 (状態の 2 倍以上) を持っています。これが、LALR(1) 文法が構文エラーを検出するために LR(1) 文法よりも多くのコードを必要とする主な理由です。そして、これら 2 つの文法に関して知っておくべきもう 1 つの重要なことは、LR(1) 文法では、reduce/reduce 競合が少ない可能性があるということです。しかし、LALR(1) では、コンフリクトを削減/削減する可能性が高くなります。

于 2015-08-10T11:44:38.227 に答える