好的,让我根据你的草图提出一个可能的错误语法:
low_postfix:
mid_infix
| low_postfix "<-"
mid_infix:
high_postfix
| mid_infix '+' high_postfix
high_postfix:
term
| high_postfix "++"
term:
ID
'(' expr ')'
只要看看var <- ++ 不是语言的一部分,就应该清楚了。唯一可以用作++ 操作数的是terms 和++ 的其他应用程序。 var <- 不是这些东西。
另一方面,var ++ <- 很好,因为<- 的操作数可以是mid_infix,也可以是high_postfix,这是++ 运算符的应用程序。
如果打算同时允许这些后缀序列,那么该语法是不正确的。
Python 语法中存在该级联的一个版本(尽管使用前缀运算符),这就是为什么not - False 可以,但- not False 是语法错误。我不愿意称它为错误,因为它可能是故意的。 (真的,这些表达都没有多大意义。)我们可以不同意这种意图的价值,但不同意 SO,它更愿意避免自以为是的讨论。
请注意,我们在此文法和 Python 文法中可能称之为“严格优先”的内容绝不限于一元运算符的组合。这是您可能从未尝试过的另一种方法:
$ python3 -c 'print(41 + not False)'
File "<string>", line 1
print(41 + not False)
^
SyntaxError: invalid syntax
那么,我们该如何解决呢?
在某种程度上,能够编写一个明确的语法来传达我们的意图会很好。当然可能写一个明确的语法,这将传达野牛的意图。但这至少是一个悬而未决的问题,它是否会向人类读者传达任何信息,因为要跟踪哪些是可接受的分组,哪些不是可接受的分组,需要大量规则的大量杂乱无章。
另一方面,使用 bison/yacc 优先级声明非常简单。我们只是按顺序列出运算符,解析器生成器会相应地解决所有歧义。 [见下文注1]
这里有一个与上面类似的语法,带有优先级声明。 (我保留了动作以防你想玩它,尽管它绝不是可重现的示例;它所依赖的基础设施比语法本身大得多,对我以外的任何人几乎没有用处。所以你'必须定义三个函数并填写一些野牛类型声明。或者只是删除AST函数并使用你自己的。)
%left ','
%precedence "<-"
%precedence "->"
%left '+'
%left '*'
%precedence NEG
%right "++" '('
%%
expr: expr ',' expr { $$ = make_binop(OP_LIST, $1, $3); }
| "<-" expr { $$ = make_unop(OP_LARR, $2); }
| expr "->" { $$ = make_unop(OP_RARR, $1); }
| expr '+' expr { $$ = make_binop(OP_ADD, $1, $3); }
| expr '*' expr { $$ = make_binop(OP_MUL, $1, $3); }
| '-' expr %prec NEG { $$ = make_unop(OP_NEG, $2); }
| expr '(' expr ')' %prec '(' { $$ = make_binop(OP_CALL, $1, $3); }
| "++" expr { $$ = make_unop(OP_PREINC, $2); }
| expr "++" { $$ = make_unop(OP_POSTINC, $1); }
| VALUE { $$ = make_ident($1); }
| '(' expr ')' { $$ = $2; }
几点说明:
-
我在一元减法生产中使用了%prec NEG,以便将该生产与减法生产分开。我还使用了%prec 声明来修改调用产生的优先级(默认为')'),尽管在这种特殊情况下这是不必要的。不过,有必要将'(' 放入优先级列表中。 ( 是用于优先比较的前瞻符号。
-
对于许多一元运算符,我在优先级列表中使用了 bison %precedence 声明,而不是 %right 或 %left。真的,一元运算符不存在关联性,所以我认为使用%precedence 更能自我记录,它不能解决涉及同一优先级的减少和转移的冲突。但是,即使一元运算符之间不存在关联性,但优先级解析算法的本质是可以将前缀运算符和后缀运算符放在相同的优先级上,并通过使用@来选择后缀或前缀运算符是否具有优先级。分别为 987654345@ 或 %left。 %right 几乎总是正确的。我用++ 做到了这一点,因为到那时我已经有点懒惰了。
这确实“有效”(我认为)。它当然可以解决所有冲突;野牛很高兴地生成了一个解析器而没有警告。我尝试的测试至少按我的预期工作:
? a++->
=> [-> [++/post a]]
? a->++
=> [++/post [-> a]]
? 3*f(a)+2
=> [+ [* 3 [CALL f a]] 2]
? 3*f(a)->+2
=> [+ [-> [* 3 [CALL f a]]] 2]
? 2+<-f(a)*3
=> [+ 2 [<- [* [CALL f a] 3]]]
? 2+<-f(a)*3->
=> [+ 2 [<- [-> [* [CALL f a] 3]]]]
但在某些表达式中,运算符优先级虽然“正确”,但可能不容易向新手用户解释。例如,尽管箭头运算符看起来有点像括号,但它们并没有那样分组。此外,在我看来,关于这两个运算符中的哪一个具有更高优先级的决定似乎完全是武断的(实际上,我的做法可能与您的预期不同)。考虑:
? <-2*f(a)->+3
=> [<- [+ [-> [* 2 [CALL f a]]] 3]]
? <-2+f(a)->*3
=> [<- [* [-> [+ 2 [CALL f a]]] 3]]
? 2+<-f(a)->*3
=> [+ 2 [<- [* [-> [CALL f a]] 3]]]
关于箭头运算符如何覆盖正常运算符优先级的问题也有些奇怪,因此您不能在不改变其含义的情况下将它们放入公式中:
? 2+f(a)*3
=> [+ 2 [* [CALL f a] 3]]
? 2+f(a)->*3
=> [* [-> [+ 2 [CALL f a]]] 3]
如果这是你的意图,那很好。这是你的语言。
请注意,存在运算符优先级问题,仅通过按优先级顺序列出运算符就不太容易解决。有时,二元运算符在左侧和右侧具有不同的绑定能力会很方便。
一个经典的(但可能有争议的)案例是赋值运算符,如果它是一个运算符。赋值必须关联到右侧(因为将a = b = 0 解析为(a = b) = 0 会很荒谬),通常的期望是它贪婪地接受尽可能多的右侧。如果赋值具有一致的优先级,那么它也会尽可能多地接受left,这似乎有点奇怪,至少对我来说。如果a = 2 + b = 7 有意义,我的直觉说它的含义应该是a = (2 + (b + 7)) [注2]。这将需要不同的优先级,这有点复杂,但并非闻所未闻。 C 通过将赋值运算符的左侧限制为(句法)左值来解决这个问题,左值不能是二元运算符表达式。但在 C++ 中,它确实意味着 a = ((2 + b) = 7),如果 2 + b 已被返回引用的函数重载,则它在语义上是有效的。
注意事项
-
优先级声明并没有真正为解析器生成器增加任何功能。它可以生成解析器的语言是完全相同的语言;它产生相同类型的解析机(下推自动机);并且至少在理论上可以取出那个下推自动机并逆向工程出它的语法。 (在实践中,这个过程产生的语法通常很可怕。但它们确实存在。)
优先级声明所做的只是根据一些用户提供的规则来解决解析冲突(通常在模棱两可的语法中)。所以值得一提的是,优先声明比编写明确的语法要简单得多。
简单的回答是,优先规则仅在发生冲突时适用。如果解析器处于只能执行一个操作的状态,那么无论优先级规则可能说什么,这就是剩下的操作。在简单的表达式语法中,中缀运算符后跟前缀运算符完全没有歧义:前缀运算符必须移位,因为以中缀运算符结尾的部分序列没有归约操作。
但是当我们编写语法时,我们必须明确指定语法中每个点可能的构造,我们通常通过定义一堆非终结符来做到这一点,每个非终结符对应于一些解析状态。表达式的明确语法已经将expression 非终结符拆分为一系列级联的非终结符,每个运算符优先级值一个。但是一元运算符在两边没有相同的约束力(因为,如上所述,一元运算符的一侧不能接受操作数)。这意味着二元运算符很可能能够为其一个操作数接受一元运算符,而不能为其另一个操作数接受相同的一元运算符。这反过来意味着我们需要再次拆分所有非终结符,对应于非终结符出现在二元运算符的左侧还是右侧。
工作量很大,而且很容易出错。如果幸运的话,错误会导致解析冲突;但同样,它可能导致语法无法识别您永远不会想到尝试的特定结构,但某些愤怒的语言用户认为这是绝对必要的。 (喜欢41 + not False)
-
我的直觉可能在我很小的时候就因为学习 APL 而被永久标记了。在 APL 中,所有的算子都向右关联,基本上没有任何优先级差异。