LR、SLR 和 LALR 解析器有啥区别?

Posted

技术标签:

【中文标题】LR、SLR 和 LALR 解析器有啥区别?【英文标题】:What is the difference between LR, SLR, and LALR parsers?LR、SLR 和 LALR 解析器有什么区别? 【发布时间】:2011-02-10 04:39:02 【问题描述】:

LR、SLR 和 LALR 解析器之间的实际区别是什么?我知道 SLR 和 LALR 是 LR 解析器的类型,但就它们的解析表而言,它们的实际区别是什么?

以及如何显示一个语法是 LR、SLR 还是 LALR?对于 LL 语法,我们只需要证明解析表的任何单元格都不应包含多个产生式规则。 LALR、SLR 和 LR 是否有类似的规则?

例如,我们如何证明语法

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

是 LALR(1) 但不是 SLR(1)?


编辑(ybungalobill):对于 LALR 和 LR 之间的区别,我没有得到满意的答案。所以 LALR 的表尺寸更小,但它只能识别 LR 语法的一个子集。有人可以详细说明 LALR 和 LR 之间的区别吗? LALR(1) 和 LR(1) 足以回答。他们都使用 1 个令牌前瞻,并且 两者 都是表驱动的!它们有何不同?

【问题讨论】:

好吧,即使我正在寻找正确的答案,LALR(1) 只是对 LR(1) 的轻微修改,其中减小了表大小,以便我们可以最小化内存用法... 【参考方案1】:

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 的语义动作,堆栈被弹出 n(从 Rn)次,对 ( S,G) 被压入堆栈,新状态 S' 设置为 GOTO(G),循环以相同的标记 T 重复。如果解析器是 SLR 解析器,则最多有一个归约规则状态等归约动作可以盲目地完成,而无需搜索以查看适用的归约。 SLR 解析器知道是否存在 减少是很有用的;这很容易判断每个状态是否明确记录了与其关联的减少次数,并且无论如何在实践中 L(AL)R 版本都需要该计数。 错误:如果 SHIFT 和 REDUCE 都不可能,则声明语法错误。

那么,如果他们都使用相同的机器,那有什么意义呢?

SLR 的价值在于其实现的简单性;您不必扫描检查前瞻集的可能减少,因为最多有一个,如果状态没有 SHIFT 退出,这是唯一可行的操作。哪个减少适用可以专门附加到状态,因此 SLR 解析机器不必寻找它。在实践中,L(AL)R 解析器可以处理大量有用的语言,而且实现起来的额外工作非常少,以至于除了作为学术练习之外,没有人实现 SLR。

LALR 和 LR 的区别在于表 generator。 LR 解析器生成器跟踪特定状态的所有可能减少及其精确的前瞻集;您最终会得到这样的状态,在这种状态下,每个归约都与其左侧上下文中的确切前瞻集相关联。这往往会建立相当大的状态集。如果 GOTO 表和减少的查找头集兼容且不冲突,则 LALR 解析器生成器愿意组合状态;这会产生相当少的状态,代价是无法区分 LR 可以区分的某些符号序列。因此,LR 解析器可以解析比 LALR 解析器更大的语言集,但解析器表要大得多。在实践中,可以找到与目标语言足够接近的 LALR 语法,以使状态机的大小值得优化; LR 解析器更好的地方由解析器外部的临时检查来处理。

所以:这三个都使用相同的机器。 SLR 是“简单”的,因为您可以忽略一点点机器,但不值得这么麻烦。 LR 解析更广泛的语言集,但状态表往往很大。这使得 LALR 成为实际的选择。

说了这么多,值得知道GLR parsers 可以解析任何上下文无关语言,使用更复杂的机器但完全相同的表(包括 LALR 使用的较小版本)。这意味着 GLR 严格来说比 LR、LALR 和 SLR 更强大;如果你能写出标准的 BNF 语法,GLR 就会根据它进行解析。机制的不同之处在于,当 GOTO 表和/或前瞻集之间存在冲突时,GLR 愿意尝试多次解析。 (GLR 如何有效地做到这一点纯粹是天才 [不是我的],但不适合这篇 SO 帖子)。

这对我来说是一个非常有用的事实。我构建程序分析器和代码转换器和解析器是必要的,但“无趣”;有趣的工作是你对解析结果所做的事情,所以重点是做解析后的工作。与破解语法以进入 LALR 可用形式相比,使用 GLR 意味着我可以相对轻松地构建工作语法。在尝试处理诸如 C++ 或 Fortran 之类的非学术语言时,这很重要,在这些语言中,您实际上需要数千条规则才能很好地处理整个语言,而且您不想花费一生来尝试破解语法规则满足LALR(甚至LR)的限制。

作为一个著名的例子,C++ 被认为是极难解析的......被进行 LALR 解析的人认为。使用 GLR 机器解析 C++ 非常简单,几乎使用 C++ 参考手册背面提供的规则。 (我正好有这样一个解析器,它不仅可以处理普通 C++,还可以处理各种供应商方言。这只有在实践中才有可能,因为我们使用的是 GLR 解析器,恕我直言)。

[2011 年 11 月编辑:我们扩展了解析器以处理所有 C++11。 GLR 让这件事变得容易多了。编辑 2014 年 8 月:现在处理所有 C++17。没有任何问题或变得更糟,GLR 仍然是猫的叫声。]

【讨论】:

AFAIK C++ 不能用 LR 解析,因为它需要无限向前看。所以我想不出任何可以用 LR 解析它的技巧。 LRE 解析器听起来也很有希望。 GCC 曾经使用 Bison == LALR 解析 C++。你总是可以用额外的 goo 来增加你的解析器来处理让你心痛的情况(前瞻,is-this-a-typename)。问题是“黑客攻击有多痛苦?”对于 GCC 来说,这非常痛苦,但他们让它发挥了作用。这并不意味着这是推荐的,这是我关于使用 GLR 的观点。 我不明白使用 GLR 如何帮助您使用 C++。如果您不知道某个东西是否是类型名称,那么您就是不知道如何解析 x * y; -- 使用 GLR 对此有何帮助? 重点是 GLR 解析器将生成 both 解析(作为集成解析“树”(实际上是 DAG)中的“模糊子树”。您可以解决稍后,通过引入其他上下文信息,您希望保留哪些子目录。我们的 C++ 解析器非常简单地处理这个问题:它不会尝试解决问题。这意味着我们不会'不必将符号表构造与解析纠缠在一起,因此我们的解析器和 C++ 的符号表构造都是单独干净的,因此每个都需要构建和维护。【参考方案2】:

LALR 解析器合并 LR 文法中的相似状态,以生成与等效 SLR 文法完全相同大小的解析器状态表,通常比纯 LR 解析表小一个数量级。但是,对于过于复杂而不能成为 LALR 的 LR 文法,这些合并状态会导致解析器冲突,或者产生无法完全识别原始 LR 文法的解析器。

顺便说一句,我在我的 MLR(k) 解析表算法 here 中提到了一些关于这个的事情。

附录

简短的回答是 LALR 解析表更小,但解析器机制是相同的。如果生成了所有 LR 状态,那么给定的 LALR 语法将生成更大的解析表,其中包含大量冗余(几乎相同)状态。

LALR 表更小,因为相似(冗余)状态合并在一起,有效地丢弃了单独状态编码的上下文/前瞻信息。优点是对于相同的语法,您可以获得更小的解析表。

缺点是并非所有 LR 语法都可以编码为 LALR 表,因为更复杂的语法具有更复杂的前瞻,导致两个或多个状态而不是单个合并状态。

主要区别在于生成 LR 表的算法在从状态到状态的转换之间携带更多信息,而 LALR 算法没有。因此 LALR 算法无法判断给定的合并状态是否真的应该保留为两个或多个单独的状态。

【讨论】:

+1 我喜欢 Honalee 的想法。我的 G/L(AL)R 解析器生成器中有类似这样的种子;它产生了最小的 LALR 机器,然后我打算拆分存在冲突的状态,但我从未执行过。这看起来像是生成最小大小的“LR”(如一组解析表)的好方法。虽然它不会帮助 GLR 解析什么,但它可能会减少 GLR 必须进行的并行解析的数量,这将是有用的。【参考方案3】:

又一个答案 (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(*) 解析器,无论您的语法需要什么。请参阅 this diagram,其中显示了 LR 解析器之间的差异。

[全面披露:LRSTAR 是我的产品]

【讨论】:

【参考方案4】:

SLR 解析器识别 LALR(1) 解析器可识别的文法真子集,进而识别 LR(1) 解析器可识别的文法真子集。

每一个都被构造为一个状态机,每个状态在解析输入时代表一组语法的产生规则(以及每个规则中的位置)。

不是 SLR 的 LALR(1) 语法的Dragon Book 示例如下:

S → L = R | R
L → * R | id
R → L

这是该语法的一种状态:

S → L•= R
R → L•

表示解析器在每个可能的产生式中的位置。它不知道它实际上是在哪个生产中,直到它到达最后并试图减少。

在这里,解析器可以移动 = 或减少 R → L

SLR(又名 LR(0))解析器将通过检查下一个输入符号是否在 Rfollow set 中(即所有终端的集合)来确定它是否可以减少在可以遵循R 的语法中。由于= 也在这个集合中,所以 SLR 解析器遇到了 shift-reduce 冲突。

但是,LALR(1) 解析器将使用可以遵循此特定产生 R 的所有终端的集合,即只有$(即输入结束)。因此,没有冲突。

正如之前的评论者所指出的,LALR(1) 解析器与 SLR 解析器具有相同数量的状态。前瞻传播算法用于从相应的 LR(1) 状态对 SLR 状态产生进行前瞻。生成的 LALR(1) 解析器可以引入 LR(1) 解析器中不存在的 reduce-reduce 冲突,但不能引入 shift-reduce 冲突。

在您的示例中,以下 LALR(1) 状态会导致 SLR 实现中的移位减少冲突:

S → b d•a / $
A → d• / c

/ 之后的符号是 LALR(1) 解析器中每个产生式的后续集合。在 SLR 中,follow(A) 包括a,也可以移位。

【讨论】:

【参考方案5】:

假设没有前瞻的解析器正在为您的语法愉快地解析字符串。

使用您给定的示例,它遇到一个字符串dc,它有什么作用?它是否将其减少为S,因为dc 是该语法产生的有效字符串?或者也许我们试图解析bdc,因为即使这是一个可接受的字符串?

作为人类,我们知道答案很简单,我们只需要记住我们是否刚刚解析了b。但是计算机很愚蠢:)

由于 SLR(1) 解析器比 LR(0) 具有额外的能力来执行前瞻,我们知道任何数量的前瞻都无法告诉我们在这种情况下要做什么;相反,我们需要回顾过去。因此,规范的 LR 解析器来救援。它会记住过去的上下文。

它记住这个上下文的方式是它自律,每当遇到b,它就会开始走上阅读bdc的道路,这是一种可能性。因此,当它看到d 时,它就知道它是否已经在走一条路了。 因此,CLR(1) 解析器可以做 SLR(1) 解析器不能做的事情!

但是现在,由于我们必须定义这么多路径,机器的状态变得非常大!

所以我们合并了看起来相同的路径,但正如预期的那样,它可能会引起混淆问题。但是,我们愿意以缩小尺寸为代价承担风险。

这是你的 LALR(1) 解析器。


现在如何通过算法来做到这一点。

当您为上述语言绘制配置集时,您会看到两种状态下的 shift-reduce 冲突。要删除它们,您可能需要考虑使用 SLR(1),它会根据后续决定做出决定,但您会发现它仍然无法做到。因此,您将再次绘制配置集,但这次有一个限制,即每当您计算闭包时,添加的额外产生式必须严格遵循。请参阅任何教科书,了解这些内容应该是什么。

【讨论】:

这不准确【参考方案6】:

使用 SLR 与 LR 生成的解析器表之间的基本区别在于,reduce 操作基于 SLR 表的 Follows 集。这可能会过于严格,最终导致轮班减少冲突。

另一方面,LR 解析器仅基于可以实际跟随非终结符被归约的终结符集做出归约决策。这组终结符通常是此类非终结符的 Follows 集的真子集,因此与换档动作发生冲突的可能性较小。

因为这个原因,LR 解析器更强大。然而,LR 解析表可能非常大。

LALR 解析器从构建 LR 解析表的想法开始,但以一种显着减小表大小的方式组合生成的状态。不利的一面是,对于一些文法,LR 表本来可以避免的冲突的可能性很小。

LALR 解析器的功能略逊于 LR 解析器,但仍然比 SLR 解析器更强大。出于这个原因,YACC 和其他此类解析器生成器倾向于使用 LALR。

附:为简洁起见,上面的 SLR、LALR 和 LR 实际表示 SLR(1)、LALR(1) 和 LR(1),因此隐含了一个标记前瞻。

【讨论】:

【参考方案7】:

除了上面的答案,这张图还展示了不同的解析器之间的关系:

【讨论】:

【参考方案8】:

除了上述答案之外,自下而上的 LR 解析器类中的各个解析器之间的区别在于,它们在生成解析表时是否会导致移位/归约或归约/归约冲突。冲突越少,语法就越强大 (LR(0)

例如,考虑以下表达式语法:

E → E + T

E → T

T→F

T → T * F

F → (E)

F → id

不是 LR(0),而是 SLR(1)。使用以下代码,我们可以构建 LR0 自动机并构建解析表(我们需要扩充语法,计算带有闭包的 DFA,计算动作集和 goto 集):

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 &gt;= 1 形式的字符串的语法是 LR(0):

A → a A b

A→c

S→A

# 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)解析器解析的:

【讨论】:

【参考方案9】:

一个简单的答案是所有 LR(1) 语法都是 LALR(1) 语法。 与 LALR(1) 相比,LR(1) 在相关的有限状态机中有更多的状态(超过两倍的状态)。这就是 LALR(1) 语法比 LR(1) 语法需要更多代码来检测语法错误的主要原因。 关于这两种文法需要了解的更重要的一点是,在 LR(1) 文法中,reduce/reduce 冲突可能更少。但在 LALR(1) 中,reduce/reduce 冲突的可能性更大。

【讨论】:

以上是关于LR、SLR 和 LALR 解析器有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章

LL,LR,SLR,LALR,LR对比与分析

LL和LR解析有啥区别?

BeautifulSoup:“lxml”、“html.parser”和“html5lib”解析器有啥区别?

作为有限状态机的通用语言解析器

习题陈意云张昱

编译原理—翻译方案属性栈代码