我正在为B编程语言编写一个编译器。这种语言的语法在语法上区分左值和右值。在将语法转换为yacc语法时,我偶然发现了reduce / reduce冲突。这是一个最小,完整且可验证的示例:
%right '['
%left '+'
%%
rvalue : '+' lvalue
| lvalue
;
lvalue : 'x'
| rvalue '[' rvalue ']'
;
Yacc表示1个减少/减少冲突。这种减少/减少冲突在状态6中找到:
6: reduce/reduce conflict (reduce 1, reduce 2) on '['
state 6
rvalue : '+' lvalue . (1)
rvalue : lvalue . (2)
. reduce 1
显然,应该选择“减少1”作为解决冲突的方法,因为“减少2”似乎永远无法成功进行解析。
如何解决此冲突?
出于便携性的考虑,我不愿意使用POSIX.1 2008中指定的功能之外的bison或yacc的任何功能。
答案 0 :(得分:2)
对于阅读此问题和答案的任何人来说,知道问题中的+
令牌旨在用作预递增运算符++
也许是有用的。根据评论,进行此更改是为了避免引入令牌声明。下面,我自由地将'+'
更改为Bison语法"++"
,因为我认为使用预期的运算符的普通拼写方式比较容易混淆。我还使用了Bison的带引号的令牌扩展名,因为它更具可读性。 (但是删除很简单。)
之所以发生冲突,是因为实际上存在使用rvalue: lvalue
生产的有效解析。具体来说,输入
++ x [ x ]
可以由您的语法以两种不同的方式进行解析:
rvalue rvalue
/ \ |
"++" lvalue lvalue
/--------------\ /------------------\
rvalue '[' rvalue ']' rvalue '[' rvalue ']'
| | / \ |
lvalue lvalue "++" lvalue lvalue
| | | |
'x' 'x' 'x' 'x'
请注意,第一个是所需的解析;下标运算符的绑定比前缀增量运算符的绑定更紧密,因此++x[x]
正确地解析为++ (x[x])
。不错,所有语言都以这种方式处理后缀运算符,这与预期的行为相符。 (绝大多数程序员希望-x[3]
首先提取数组x
的元素3,然后取反。首先绑定-x
毫无意义。对于++
;如果x
是一个数组,那么++x
的意义就不如-x
。)
与您主张应选择“减少1”相反;正确的分析要求采用“ reduce 2”。该错误也反映在您的优先级声明中,该声明在逻辑上应该为后缀运算符赋予右相关联的优先级:
%right "++" '['
(从技术上讲,前缀运算符的绑定不如后缀运算符绑定。但是由于具有右关联性,它们可以共享优先级。)
但是没有必要进行更改,因为优先级声明无法解决减少/减少冲突,因为按优先级进行的解析始终涉及对可以减少的产生的优先级和优先级之间的比较。 代币的前瞻性标记可以更改。 (换句话说,被比较的事物的类型是不同的。)
在状态6(问题中再现)中,解析器先将"++"
移至'x'
,然后将'x'
强制降为lvalue
。因此,解析器堆栈为... "++" lvalue
,而超前标记为[
。如果语法没有尝试分离左值和右值(以便堆栈的顶部仅为value
而不是lvalue
),则解析器可用的选择是减少{{1} }移至"++" value
,或移动value
以准备右侧[
。使用上述优先级声明,由于右关联性,移位将获胜,因此将出现正确的解析。
但是语法试图区分左值和右值,这使得解析器无法移动value '[' value ']'
;为了使[
有效,必须先 将[
减小为lvalue
。但是,优先决策始终是即时的;解析器并不真正认为rvalue
的减少是某种程度上成为转移rvalue: lvalue
的序幕。它看到的是两个相互竞争的reduce动作,并且优先级不适用于此类冲突。
由于优先声明对解决这种特殊冲突没有帮助,因此最简单的方法是避免尝试将它们用于一元运算符,而将其保留用于二进制运算符。 (也可能根本不使用它们,但是它们对于表达二进制优先级很方便。)B reference manual [注1]清楚地表明,叙述文本而非所包含的语法是精确定义的内容运算符优先级和关联性,并且叙述文本包括两个语法类别:主表达式和一元表达式,它们没有出现在语法中,但实际上在语法上是必需的
如果我们忽略左值/右值的区别,那么使用这些非终结符编写语法很容易,因此这是一个不错的起点。 (请注意:为了避免依赖优先级声明,我将后增/减运算符移到[
中。)
primary
现在我们可以看到,有两个不同的非末端需要分为 l 和 r 变体,因为%token NAME CONSTANT
%token INC "++" DEC "--"
%left '+' '-'
%left '*' '/' '%'
%start value
%%
primary : NAME
| primary '[' value ']'
| CONSTANT
| '(' value ')'
| primary "++"
| primary "--"
unary : primary
| '*' unary
| '-' unary
| '&' unary
| "++" unary
| "--" unary
value : unary
| value '+' value
| value '-' value
| value '*' value
| value '/' value
| value '%' value
和{{ 1}}可以产生左值。 (分别为primary
和unary
。)由于级联,它并不像将这两个非终端都分为两类一样简单:
x[x]
与所需的将左值隐式简化为右值相结合。
我们的第一个想法可能是拆分非终端,让级联流过*x
生成:
value : unary
unary : primary
不幸的是,这会产生两条到达rvalue: lvalue
的不同路径:
value : runary
runary : lunary
| rprimary
lunary : lprimary
rprimary: lprimary
由于级联生产没有关联的动作,并且从左值到右值的转换(解除引用操作)在两种情况下都是相同的,因此实际上对我们而言,选择这两个路径没有什么区别。但是解析器会在意,因此我们必须消除其中之一。这是一种可能的解决方案:
lprimary