Answers:
在较高的层次上,LL解析和LR解析之间的区别是LL解析器从开始符号开始,并尝试应用产生式来到达目标字符串,而LR解析器从目标字符串开始,并尝试返回到开始字符串符号。
LL解析是从左到右,最左边的推导。也就是说,我们考虑从左到右的输入符号,并尝试构造最左侧的导数。这是通过从开始符号开始并反复扩展最左边的非终结符来完成的,直到我们到达目标字符串为止。LR解析是从左到右,最右边的派生,这意味着我们从左到右扫描并尝试构建最右边的派生。解析器连续选择输入的子字符串,并尝试将其反向返回非终端。
在LL解析期间,解析器在两个动作之间连续选择:
例如,给出以下语法:
int
然后给定字符串int + int + int
,LL(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
请注意,在每个步骤中,我们都会查看生产中最左边的符号。如果它是终端,则将其匹配,如果它是非终端,则可以通过选择规则之一来预测它的状态。
在LR解析器中,有两个操作:
例如,一个LR(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
您提到的两种解析算法(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解析的更多信息,我刚刚完成了编译器课程的教学,并在课程网站上提供了一些有关解析的讲义和讲座幻灯片。如果您认为有用的话,我很乐于详细介绍。
乔什•哈伯曼(Josh Haberman)在他的文章LL和LR解析神秘的文章中声称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解析器输出后序遍历。
有关深入的解释,请参见示例和结论,以查看Haberman的文章。
与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解析器无法同时处理两个规则,因此必须选择FunctionDef或FunctionDecl。但是要知道哪个是正确的,它必须提前寻找一个';'。要么 '{'。在语法分析时,前瞻(k)似乎是无限的。在解析时,它是有限的,但可能很大。
LR解析器不必提前,因为它可以同时处理两个规则。因此LALR(1)解析器生成器可以轻松处理此语法。
给定输入代码:
int main (int na, char** arg);
int main (int na, char** arg)
{
}
LR解析器可以解析
int main (int na, char** arg)
不在乎遇到“;”之前要识别什么规则 或“ {”。
LL解析器挂在'int'上,因为它需要知道识别哪个规则。因此,它必须提前查找“;” 要么 '{'。
LL解析器的另一个噩梦是语法的递归。左递归在语法上是很正常的事情,对于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)