木兰语言 0.0.17.2 实现简易网页浏览器,又一次碰到语法歧义

上周试用木兰语言加 QtWebkit 实现简易网页浏览器(已开源在 Gitee ,81 行代码)时,发现需要复现带参数的 super:

super(演示, self).__init__()

实现过程中,又一次遇到了这个头疼的报错:

ParserGeneratorWarning: 1 shift/reduce conflict 

去年碰到过几次,都是通过照着逆向工程设置词的优先级来规避,没有深究调试方法,这次决定下点功夫搞清楚缘由。

rply 的调试信息有限,貌似 七年前就是如此,包括 shift/reduce 在内的所有信息都极简,项目现在似乎也没有什么改进的迹象。由于它是参考 ply 重写的,于是查看了 ply 对歧义语法的调试信息示例。纠结了一下是把木兰用 ply 重写还是修改 rply 来获得类似调试信息,决定走后者。

这里记一下:任何框架或工具,包括编程语言,可调试性——即告知用户“哪儿出错了?”——对于可用性非常非常重要。任何的反馈信息,包括警告、报错,都应以用户可理解、问题可定位、提供解决方案为首要目标。

首先,分别创建了 plyrply 的最简歧义演示。语法如下(ply 不支持在语法定义中使用中文标识符,下面是 rply 的表示):

表达式 : 数
表达式 : 表达式 减 表达式 

测试表达式为 2 - 3 - 4。在没有优先级设置时,会报警:1 shift/reduce conflict 而且输出:3

在 ply 的 parser.out 文件中,可以看到进一步细节:

state 4

    (1) expression -> expression MINUS expression .
    (1) expression -> expression . MINUS expression

  ! shift/reduce conflict for MINUS resolved as shift
    $end            reduce using rule 1 (expression -> expression MINUS expression .)
    MINUS           shift and go to state 3

  ! MINUS           [ reduce using rule 1 (expression -> expression MINUS expression .) ] 

根据 ply 官方文档,可以估摸出是第二个减号有歧义,可以先将 2-3 化简为“expression”,也可以将减号接在 3 后面,作为 expression . MINUS expression 的一部分。在有此类歧义时,默认 shift 而不化简。因此,待 3-4 都解析完后化为 expression,求值为 -1,再将 2-(-1) 化简求值为 3。

这里注意到,这个报警信息与具体的测试表达式无关。在语法分析器生成时,会根据语法定义生成所有可能的语法要素序列,在此基础上发现所有歧义。另一方面也好奇,是否能够进一步生成能够复现歧义的表达式用例,这样应该可以更方便开发者调试吧。

接下来,就是在 rply 源码内加入调试信息输出(开源在此)。可以看到下面的 5 个序列:

0[LRItem(S' -> . 表达式), LRItem(表达式 -> . 数), LRItem(表达式 -> . 表达式 减 表达式)]
1[LRItem(表达式 -> 数 .)]
2[LRItem(S' -> 表达式 .), LRItem(表达式 -> 表达式 . 减 表达式)]
3[LRItem(表达式 -> 表达式 减 . 表达式), LRItem(表达式 -> . 数), LRItem(表达式 -> . 表达式 减 表达式)]
4[LRItem(表达式 -> 表达式 减 表达式 .), LRItem(表达式 -> 表达式 . 减 表达式)] 

以及歧义相关的序列和词:4 》》 '减',即:4[LRItem(表达式 -> 表达式 减 表达式 .), LRItem(表达式 -> 表达式 . 减 表达式)] 中的减号,句点含义与 ply 中相同,这样包含的信息已接近了 ply 的。

回头看 super 语法,整理后的输出如下:

词'('有歧义,默认进行 shift2
歧义序列:
        超类: super .
        超类: super . 实参部分
        实参部分: . ( )
        实参部分: . ( 各实参 ) 

仍然看不大清,简化了 实参部分 语法后,仍然有歧义:

词'('有歧义,默认进行 shift2
歧义序列:
        超类: super .
        超类: super . 实参部分
        实参部分: . ( ) 

继续简化:

词'('有歧义,默认进行 shift2
歧义序列:
        超类: super .
        超类: super . ( ) 

到这里看出,其实很简单,没有优先级设置时,super() 有两种解析方式:

步骤符号栈词输入行为
1super ( )移入 super
2super( )歧义:
1)根据 超类: super,可以直接消减(reduce)为 超类2) 根据 超类: super ( ),可以移入 (

问题确定之后,就是如何确定优先级高低,待继续研究。

编辑于 2021-05-03 04:54