K语言入门学习4:消除有歧义的解析

这节课的目的是教大家如何使用K的内置特性来消除歧义,将歧义语法规范转换成明确表达AST的语法规范。

优先级块

在实践中,很少有自然语言处理领域之外的形式化语言是模棱两可的。主要原因是,解析明确的语言比解析模糊的语言要快得多。相反,编程语言设计者通常使用运算符优先级和关联的概念来使表达式语法规范没有歧义。这些机制的工作原理是,在出现歧义时,指示解析器拒绝某些AST,而采用其他AST; 通常,使用这些技术可以消除语法规范中的“所有”歧义。

虽然有时可以显式地重写语法规范来删除这些解析,但由于K语言的语法规范和AST生成是不可避免地联系在一起的,所以通常不鼓励这样做。相反,我们使用显式表示不同操作符,在不同情况下拥有相应优先级的方法来解决歧义。

例如,在C语言中,&&要优先于||,这就意味着表达式true && false || false 只会有一个有效的AST:(true && false) || false

下面我们就来看看,基于语法规范的第3次迭代(lesson-04-a.k):

module LESSON-04-A

  syntax Boolean ::= "true" | "false"
                   | "(" Boolean ")" [bracket]
                   > "!" Boolean [function]
                   > Boolean "&&" Boolean [function]
                   > Boolean "^" Boolean [function]
                   > Boolean "||" Boolean [function]

endmodule

在本例中,在单个块中分隔生产的一些|符号已被替换为>。这是用来描述与这个生产块关联的优先级组

在这个例子中,第一个优先级组由语言的原子组成:truefalse和括号操作符。通常,优先级组从::=>操作符开始,扩展到下一个>操作符或生产块的末尾。因此,我们可以看到,该语法规范中的第二、第三、第四和第五优先级组都由一个生产组成。

在解析程序时,这些优先级组的含义变得明确显起来:具有较小优先级的符号(即,绑定较松的的符号)不能作为具有较高优先级的符号的直接子级(即,绑定较紧的的符号)出现。在这种情况下,>操作符可以被看作是一个大于的操作符,描述了对生产块中的生产的可传递的部分排序,表达了它们的相对优先级。

为了更具体地了解这一点,让我们再次看看程序true && false || false。如前所述,以前这个程序是不明确的,因为解析器可以选择&&||的子元素,反之亦然。然而,由于优先级较低的符号(即||)不能作为优先级较高的符号(即&&)的直接子元素出现,解析器将拒绝 ||位于&&操作符下的解析。结果,我们只剩下明确的解析(true && false) || false。类似地,true || false && false可以明确地解析为true || (false && false) 。相反,如果用户显式地想要另一个解析,他们可以用括号来表示,显式地写true && (false || false) 。这仍然可以成功解析,因为||操作符不再是&&操作符的直接子操作符,而是()操作符的直接子操作符,而&&操作符是一个间接的父操作符,不受优先级限制。

然而,聪明的读者已经注意到一个矛盾:我们已经定义了()也比||具有更大的优先级。有人可能会认为这应该意味着||不能作为()的直接子元素出现。这是一个问题,因为优先级组被应用于每一个可能的单独解析。也就是说,即使术语在此消除歧义规则之前是明确的,如果它违反了优先级规则,我们仍然拒绝该解析。

然而,事实上,我们并不将此程序作为一个解析错误拒绝。这是为什么呢?优先级规则比之前描述的稍微复杂一些。事实上,它只是有条件地适用。具体来说,它适用于子节点是父节点生产中的第一个或最后一个生产项的情况。例如,在生产Bool "&&" Bool”中,第一个Bool non-terminal前面没有任何terminals,最后一个Bool non-terminal后面不跟随任何terminals。因此,我们将优先级规则应用于&&的两个子元素。但是,在’()'操作符中,唯一的non-terminal前面和后面都有terminals。因此,当()是父元素时,就不适用于优先级规则。正因为如此,我们上面提到的程序能够成功解析。

练习

使用kast解析程序true && false || false,并确认AST将||放置为顶级符号。然后修改K定义,以便你将获得不一样的解析。

关联

即使将表达式语法分解为优先级块,得到的语法规范仍然可能是不明确的。如果我们尝试解析下面的程序(’ assoc.bool '),就可以看到这一点:

true && false && false

优先级块在这里帮不了我们:问题出现在两个解析之间,两个可能的解析都有一个直接的父节点和子节点,它们都在一个优先级块中(在这种情况下,&&和它本身在同一个块中)。

这就是关联律发挥作用的地方。关联性将以下附加规则应用于解析:

  • 左关联符号不能作为具有同等优先级的符号的最右边的子符号出现;
  • 右关联符号不能作为具有同等优先级的符号的最左边的子元素出现; 以及
  • 非关联符号不能作为具有同等优先级的符号的最左边的子元素或最右边的子元素出现。

在C语言中,二元操作符都是左结合的,这意味着表达式true && false && false可以明确地解析为(true && false) && false,因为&&不能作为其最右边的子元素出现。

下面来看看例子K定义的第4次迭代 (lesson-04-b.k):

module LESSON-04-B

  syntax Boolean ::= "true" | "false"
                   | "(" Boolean ")" [bracket]
                   > "!" Boolean [function]
                   > left: Boolean "&&" Boolean [function]
                   > left: Boolean "^" Boolean [function]
                   > left: Boolean "||" Boolean [function]

endmodule

在这里,每个优先级组紧接在::=>操作符之后,可以后跟一个表示该优先级组结合性的符号:left:用于左结合性,right:用于右结合性,或者non-assoc:用于非结合性。在这个例子中,我们应用结合性的每个优先级组只有一个生产,但我们同样可以编写一个具有多个生产和一个结合性的优先级块。

例如,请看源码lesson-04-c.k

module LESSON-04-C

  syntax Boolean ::= "true" | "false"
                   | "(" Boolean ")" [bracket]
                   > "!" Boolean [function]
                   > left: 
                     Boolean "&&" Boolean [function]
                   | Boolean "^" Boolean [function]
                   | Boolean "||" Boolean [function]

endmodule

在本例中,与上一个例子的不同,&&^ ||具有相同的优先级。但是,作为一个组来看,整个组是左关联的。这意味着&&^ || 不能作为&&^ ||的右子元素出现。因此,这种语法也没有歧义。但是,它表达了一种不同的语法规范,我们鼓励你在实践中考虑其中的差异。

练习

您自己解析程序true && false && false,并确认AST将最右边的“&&”放在表达式的顶部。然后修改K定义,以产生另一个解析。

显性优先级和关联声明

前面我们只考虑了这样一种情况,即您希望表达优先级或关联关系的所有结果都位于同一个结果块中。然而,在实践中,这并不总是可行或可取的,特别是当一个定义跨多个模块增长时,怎么办?

因此,K语言提供了第二种声明优先级和结合关系的方法。

考虑下面的语法,我们将其命名为lesson-04-d.k。和表示的语法与lesson-04-b.k完全相同:

module LESSON-04-D

  syntax Boolean ::= "true" [literal] | "false" [literal]
                   | "(" Boolean ")" [atom, bracket]
                   | "!" Boolean [not, function]
                   | Boolean "&&" Boolean [and, function]
                   | Boolean "^" Boolean [xor, function]
                   | Boolean "|" Boolean [or, function]

  syntax priorities literal atom > not > and > xor > or
  syntax left and
  syntax left xor
  syntax left or
endmodule

这引入了k语言的一些新特性。首先,我们看到了一些我们还没有认识到的属性。这些实际上不是内置属性,而是用于在概念上将生产分组在一起的用户定义属性。例如,“syntax priorities”句子中的“literal”被用来指具有“literal”属性的结果,即truefalse

一旦我们理解了这一点,理解这条语法的意思就变得相对简单了。每个“语法优先级”句子定义了一个优先级关系,其中每个“>”分隔了一个优先级组,该优先级组包含所有具有该组中至少一个属性的结果,每个“语法左”、“语法右”、或者“syntax non-assoc”句子定义了一种关联关系,将所有具有一个目标属性的结果连接到一个左、右或非关联的分组中。

偏好 / 规避

有时优先级和结合性不足以消除语法歧义。特别是,有时故意希望能够直接在两个歧义解析之间进行选择,而如果所解析的术语是明确的,则仍然不拒绝任何解析。这方面的一个很好的例子是命令式c语言中著名的“悬挂其他东西”问题。

来看下面的定义 (lesson-04-E.k):

module LESSON-04-E

  syntax Exp ::= "true" | "false"
  syntax Stmt ::= "if" "(" Exp ")" Stmt
                | "if" "(" Exp ")" Stmt "else" Stmt
                | "{" "}"
endmodule

当我们写出以下的程序(dangling-else.if):

if (true) if (false) {} else {}

这是模棱两可的,因为不清楚else子句是外部if的一部分还是内部if的一部分。首先,我们可以尝试通过优先级来解决这个问题,即不带else的if不能作为带else的if的子元素出现。但是,由于父符号中的non-terminal前后都有terminal,所以优先级的方案将不起作用。

相反,当歧义出现时,我们可以通过告诉解析器偏好(prefer)避免(avoid)某些结果来直接解决歧义。例如,当我们解析上述程序时,我们看到下面的歧义是一条错误消息:

[Error] Inner Parser: Parsing ambiguity.
1: syntax Stmt ::= "if" "(" Exp ")" Stmt

`if(_)__LESSON-04-E_Stmt_Exp_Stmt`(`true_LESSON-04-E_Exp`(.KList),`if(_)_else__LESSON-04-E_Stmt_Exp_Stmt_Stmt`(`false_LESSON-04-E_Exp`(.KList),`;_LESSON-04-E_Stmt`(.KList),`;_LESSON-04-E_Stmt`(.KList)))
2: syntax Stmt ::= "if" "(" Exp ")" Stmt "else" Stmt

`if(_)_else__LESSON-04-E_Stmt_Exp_Stmt_Stmt`(`true_LESSON-04-E_Exp`(.KList),`if(_)__LESSON-04-E_Stmt_Exp_Stmt`(`false_LESSON-04-E_Exp`(.KList),`;_LESSON-04-E_Stmt`(.KList)),`;_LESSON-04-E_Stmt`(.KList))
        Source(./dangling-else.if)
        Location(1,1,1,30)

粗略地说,我们可以看到歧义存在于if带一个elseif不带else之间。因为我们想要选择第一个解析,我们可以告诉K使用avoid属性来“避免”第二个解析。考虑以下修改后的定义(lesson-04-f.k):

module LESSON-04-F

  syntax Exp ::= "true" | "false"
  syntax Stmt ::= "if" "(" Exp ")" Stmt
                | "if" "(" Exp ")" Stmt "else" Stmt [avoid]
                | "{" "}"
endmodule

在这里,我们向else生产添加了avoid属性。因此,当出现歧义时,一个或多个可能的解析在解析的歧义部分的顶部有该符号,我们从考虑中删除这些解析,只考虑剩下的那些。’ prefer '属性的行为类似,但会删除所有不具有该属性的解析。在这两种情况下,如果解析不是二义性,则不会采取任何操作。

练习

  1. 使用lesson-04-f.k解析程序if (true) if (false) {} else{} 。并确认else子句是最里面的if语句的一部分。然后修改定义,以便您将获得另一种的解析。
  2. 修改上堂课第2题的解,使一元否定的绑定比乘法和除法的绑定更紧密,而乘法和除法的绑定比加法和减法的绑定更紧密,并且每个二元运算符都应该是左结合的。内联或显式地编写这些优先级和关联性声明。
  3. 写一个简单的语法规范,至少包含一个不能通过优先级或结合性解决的歧义,然后使用“prefer”属性来解决这个歧义。

猜你喜欢

转载自blog.csdn.net/DongAoTony/article/details/124918625