微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

组合具有不同优先级的一元运算符 注意事项

如何解决组合具有不同优先级的一元运算符 注意事项

我在 Bison 创建操作符时遇到了一些问题: <- = 具有低优先级的身份后缀运算符,以强制首先评估左侧的内容,例如1+2<-*3(等价于 (1+2)*3)以及 ->一个前缀运算符,它做同样的事情,但在右边。

我无法使语法正常工作并使用 - not False 使用 Python 进行测试,这导致了语法错误(在 Python 中,- 的优先级高于 not )。但是,这在 C 或 C++ 中不是问题,其中 -!/not 具有相同的优先级。

当然,优先级的差异与这两个运算符之间的关系无关,只是与其他运算符的关系导致它们之间具有相对优先级。

为什么在解析时链接具有不同优先级的前缀或后缀运算符会出现问题,以及如何实现 <--> 运算符,同时仍然具有更高优先级的运算符,例如 !、{{ 1}}、++ 等?

Obligatory Bison(此模式对所有运算符重复,其中 NOT 的优先级高于 copy):

post_unary

此类别中的链接运算符,例如post_unary: copy | post_unary "++" | post_unary "--" | post_unary '!' ; 在语法上运行良好。

解决方法

好的,让我根据您的草图建议一个可能的错误语法:

low_postfix:
    mid_infix
|   low_postfix "<-"
mid_infix:
    high_postfix
|   mid_infix '+' high_postfix
high_postfix:
    term
|   high_postfix "++"
term:
    ID
    '(' expr ')'

只要看看那些产生式,就应该很清楚 var <- ++ 不是语言的一部分。唯一可以用作 ++ 的操作数的是 term++ 的其他应用程序。 var <- 两者都不是。

另一方面,var ++ <- 很好,因为 <- 的操作数可以是 mid_infix,它可以是 high_postfix,它是 {{ 1}} 运算符。

如果意图是允许两个 if 那些后缀序列,那么该语法是不正确的。

该级联的一个版本存在于 Python 语法中(尽管使用了前缀运算符),这就是为什么 ++ 没问题,但 not - False 是语法错误的原因。我不愿意称其为错误,因为它可能是故意的。 (实际上,这两种表达都没有多大意义。)我们可以不同意这种意图的价值,但不同意 SO,后者更愿意避免自以为是的讨论。

请注意,我们在此语法和 Python 语法中可能称为“严格优先级”的内容绝不限于一元运算符的组合。这是您可能从未尝试过的另一种:

- not False

那么,我们该如何解决这个问题?

在某种程度上,能够编写一个明确的语法来传达我们的意图会很好。并且肯定可能编写一个明确的语法,将意图传达给野牛。但是,对于它是否会向人类读者传达任何信息,这至少是一个悬而未决的问题,因为为了跟踪哪些是可接受的分组和哪些不是可接受的分组,需要大量杂乱无章的多个规则。

另一方面,使用 bison/yacc 优先声明非常简单。我们只是按顺序列出运算符,解析器生成器会相应地解决所有歧义。 [见下面的注释 1]

这里有一个与上面类似的语法,带有优先级声明。 (我把这些动作留在原地,以防你想玩它,尽管它绝不是一个可复制的例子;它所依赖的基础设施比语法本身大得多,而且对我以外的任何人都没有什么用处。所以你必须定义这三个函数并填写一些野牛类型声明。或者只是删除 AST 函数并使用您自己的函数。)

$ python3 -c 'print(41 + not False)'
  File "<string>",line 1
    print(41 + not False)
                 ^
SyntaxError: invalid syntax

一些注意事项:

  1. 我在一元减产生式上使用了 %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,$3); } | expr '*' expr { $$ = make_binop(OP_MUL,$3); } | '-' expr %prec NEG { $$ = make_unop(OP_NEG,$2); } | expr '(' expr ')' %prec '(' { $$ = make_binop(OP_CALL,$3); } | "++" expr { $$ = make_unop(OP_PREINC,$2); } | expr "++" { $$ = make_unop(OP_POSTINC,$1); } | VALUE { $$ = make_ident($1); } | '(' expr ')' { $$ = $2; } 以便将该产生式与减法产生式分开。我还使用了 %prec NEG 声明来修改调用产生式的优先级(默认值为 %prec),尽管在这种特殊情况下这是不必要的。不过,有必要将 ')' 放入优先级列表中。 '(' 是用于优先级比较的先行符号。

  2. 对于许多一元运算符,我在优先级列表中使用了 bison ( 声明,而不是 %precedence%right。真的,没有一元运算符的结合性这样的东西,所以我认为使用 %left 更能自我记录,它不能解决涉及相同优先级的减少和移位的冲突。但是,即使一元运算符之间不存在关联性,但优先解析算法的本质是可以将前缀运算符和后缀运算符置于同一优先级,并通过使用 { 来选择后缀运算符或前缀运算符是否具有优先级分别为 {1}} 或 %precedence%right 几乎总是正确的。我用 %left 做到了这一点,因为到那时我已经有点懒惰了。

这确实“有效”(我认为)。它当然解决了所有的冲突; bison 愉快地生成了一个没有警告的解析器。我尝试的测试至少按我的预期工作:

%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] 会很荒谬),通常的期望是它贪婪地接受尽可能多的右边。如果分配具有一致的优先级,那么它也会尽可能多地接受 left ,这似乎有点奇怪,至少对我来说。如果 a = b = 0 是有意义的,我的直觉说它的意义应该是 (a = b) = 0 [注 2]。这将需要不同的优先级,这有点复杂但并非闻所未闻。 C 通过将赋值运算符的左侧限制为(语法)左值来解决这个问题,左值不能是二元运算符表达式。但在 C++ 中,它确实表示 a = 2 + b = 7,如果 a = (2 + (b + 7)) 已被返回引用的函数重载,则在语义上是有效的。


注意事项

  1. 优先级声明并没有真正为解析器生成器增加任何功能。它可以生成解析器的语言是完全相同的语言;它产生相同类型的解析机(下推自动机);并且至少在理论上可以采用下推自动机并从中逆向工程一个语法。 (在实践中,这个过程产生的语法通常是可怕的。但它们确实存在。)

    优先级声明所做的只是根据一些用户提供的规则解决解析冲突(通常在歧义语法中)。因此,值得一提的是,使用优先级声明然后编写明确的语法要简单得多。

    简单的回答是,优先规则仅在发生冲突时适用。如果解析器处于只能执行一个操作的状态,那么无论优先级规则如何规定,都会保留该操作。在简单的表达式语法中,中缀运算符后跟前缀运算符完全没有歧义:前缀运算符必须移位,因为对于以中缀运算符结尾的部分序列没有减少操作。

    但是当我们在写一个语法时,我们必须明确指定在语法的每一点上什么构造是可能的,我们通常通过定义一堆非终结符来实现,每个非终结符对应于一些解析状态。表达式的明确语法已经将 a = ((2 + b) = 7) 非终结符拆分为一系列非终结符级联,每个运算符优先级值一个。但是一元运算符在两侧的约束力不同(因为如上所述,一元运算符的一侧不能接受操作数)。这意味着二元运算符很可能能够为其一个操作数接受一元运算符,而不能为其另一个操作数接受相同的一元运算符。这反过来意味着我们需要再次拆分所有非终结符,对应于非终结符出现在二元运算符的左侧还是右侧。

    这项工作量很大,而且很容易出错。如果幸运的话,这个错误会导致解析冲突;但同样它可能导致语法无法识别您永远不会想到尝试的特定结构,但某些愤怒的语言用户认为这是绝对必要的。 (比如2 + b

  2. 我的直觉可能在很小的时候就被学习 APL 永久标记了。在 APL 中,所有运算符都关联到右侧,基本上没有任何优先级差异。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。