为了任何阅读此问题和答案的人的利益,知道问题中的 + 标记旨在成为预增量运算符 ++ 可能很有用。根据评论,进行更改是为了避免引入令牌声明。下面,我冒昧地将 '+' 更改为 Bison 语法 "++",因为我认为使用预期运算符的正常拼写不会造成混淆。我也确实使用了 Bison 的quoted-token 扩展,因为它更具可读性。 (但删除起来很简单。)
发生冲突是因为实际上存在使用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 完全没有意义。对于 ++ 来说同样如此;如果 @ 987654335@ 是一个数组,++x 和-x 一样没有意义。)
这与您应该选择“减少 1”的断言相反;正确的解析要求采用“减少 2”。该错误也反映在您的优先声明中,从逻辑上讲,它应该以右关联的方式将优先权赋予后缀运算符:
%right "++" '['
(从技术上讲,前缀运算符的绑定不如后缀运算符紧密。但由于右关联性,它们可以共享优先级。)
但是进行这种更改是没有意义的,因为优先级声明无法解决减少/减少冲突,因为优先级解决总是涉及可以减少的生产的优先级与优先级之间的比较可以移动的前瞻 token。 (也就是说,被比较的事物的类型不同。)
在状态 6(在问题中重现)中,解析器已将 "++" 移位,然后将 'x' 移位,然后将 'x' 强制归约到 lvalue。所以解析器堆栈是... "++" lvalue,前瞻标记是[。如果语法没有尝试分离左值和右值(因此堆栈的顶部只是value 而不是lvalue),那么解析器可用的选择是将"++" value 减少到value,或转移[ 以准备右侧value '[' value ']'。使用上述优先级声明,由于右关联性,移位会获胜,因此会出现正确的解析。
但是语法试图区分左值和右值,这使得解析器无法移动[;要使[ 有效,它必须首先将lvalue 减少为rvalue。然而,优先级决定总是即时的;解析器并没有真正将rvalue: lvalue 减少视为转移[ 的前奏。它看到的是两个相互竞争的 reduce 操作,优先级不适用于此类冲突。
由于优先声明不会帮助解决这个特定的冲突,最简单的方法是避免尝试将它们用于一元运算符,而保留它们用于二元运算符。 (也可以根本不使用它们,但它们对于表达二进制优先级很方便。)B reference manual [注 1] 清楚地表明,叙述文本,而不是包含的语法,是精确定义运算符优先级的内容和结合性,叙述文本包括两个句法类别,初级表达式和一元表达式,它们没有出现在语法中,但实际上在句法上是必要的。
如果我们忽略左值/右值的区别,使用这些非终结符编写语法很容易,所以这是一个很好的起点。 (注意:我将后自增/自减运算符移至 primary 以避免依赖优先级声明。)
%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
现在我们可以看到有两个不同的非终结符需要拆分为 l 和 r 变体,因为 primary 和 unary 都可以产生左值。 (分别为x[x] 和*x。)但是,由于级联,这并不像将这两个非终端分为两类那么简单:
value : unary
unary : primary
结合所需的将左值隐式简化为右值。
我们的第一个想法可能是只拆分非终端,让级联流过rvalue: lvalue 产品:
value : runary
runary : lunary
| rprimary
lunary : lprimary
rprimary: lprimary
不幸的是,这会产生两条不同的路径来到达lprimary:
value -> runary -> lunary -> lprimary
value -> runary -> rprimary -> lprimary
由于级联产生式没有关联的操作,并且左值到右值的转换(取消引用操作)对于两个实例都是相同的,因此选择这些路径中的哪一个实际上对我们没有影响。但是解析器会关心,所以我们必须消除其中一个。这是一种可能的解决方案:
%token NAME CONSTANT
%token INC "++" DEC "--"
%left '+' '-'
%left '*' '/' '%'
%start value
%%
lprimary: NAME
| primary '[' value ']'
primary : lprimary
| rprimary
rprimary: CONSTANT
| '(' value ')'
| lprimary "++"
| lprimary "--"
lunary : lprimary
| '*' runary
runary : lunary
| rprimary
| '-' runary
| '&' runary
| "++" lunary
| "--" lunary
value : runary
| value '+' value
| value '-' value
| value '*' value
| value '/' value
| value '%' value