尤其是在学习时,拥有调试功能非常重要。幸运的是,YACC 可以提供很多反馈。这种反馈会带来一些开销,因此您需要提供一些开关来启用它。
在编译语法时,将 --debug 和 --verbose 添加到 YACC 命令行。在您的语法 C 头文件中,添加以下内容
int yydebug=1;
这将生成文件 'y.output',其中解释了创建的状态机。
当您现在运行生成的二进制文件时,它将输出 *大量* 正在发生的事情。 这包括状态机当前所处的状态,以及正在读取的标记。
Peter Jinks 撰写了一个关于 调试 的页面,其中包含一些常见错误以及如何解决它们。
在内部,您的 YACC 解析器运行一个所谓的“状态机”。顾名思义,这是一个可以处于多种状态的机器。然后有一些规则来管理从一个状态到另一个状态的转换。一切都从我之前提到的所谓“根”规则开始。
引用示例 7 y.output 的输出
state 0
ZONETOK , and go to state 1
$default reduce using rule 1 (commands)
commands go to state 29
command go to state 2
zone_set go to state 3
默认情况下,此状态使用 'commands' 规则进行归约。这是前面提到的递归规则,它定义了 'commands' 由单个命令语句构建,后跟一个分号,然后可能还有更多命令。
此状态会一直归约,直到遇到它能理解的东西,在本例中是 ZONETOK,即单词 'zone'。然后它进入状态 1,该状态进一步处理 zone 命令。
state 1
zone_set -> ZONETOK . quotedname zonecontent (rule 4)
QUOTE , and go to state 4
quotedname go to state 5
第一行中有一个 '.' 来指示我们所处的位置:我们刚刚看到了 ZONETOK,现在正在寻找 'quotedname'。显然,quotedname 以 QUOTE 开头,这会将我们发送到状态 4。
要进一步了解这一点,请使用调试部分中提到的标志编译示例 7。
每当 YACC 警告您存在冲突时,您可能会遇到麻烦。解决这些冲突似乎有点像一门艺术,它可能会让您学到很多关于您的语言的知识。可能比您想知道的还要多。
问题围绕如何解释标记序列展开。假设我们定义一种需要接受以下两个命令的语言
delete heater all
delete heater number1
为此,我们定义以下语法
delete_heaters:
TOKDELETE TOKHEATER mode
{
deleteheaters($3);
}
mode: WORD
delete_a_heater:
TOKDELETE TOKHEATER WORD
{
delete($3);
}
您可能已经预感到麻烦了。状态机首先读取单词 'delete',然后需要根据下一个标记决定去哪里。下一个标记可以是模式,指定如何删除加热器,也可以是要删除的加热器的名称。
然而,问题在于对于这两个命令,下一个标记都将是 WORD。因此,YACC 不知道该怎么做。这会导致 'reduce/reduce' 警告,以及另一个警告,即 'delete_a_heater' 节点永远不会被访问到。
在这种情况下,冲突很容易解决(例如,通过将第一个命令重命名为 'delete heaters all',或者通过将 'all' 作为单独的标记),但有时会更难。当您将 --verbose 标志传递给 yacc 时生成的 y.output 文件可能会有很大帮助。