这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战
本文为译文,原文链接:dinosaur.compilertools.net/yacc/
接上文,继续下一节。
4:Parser如何工作?
yacc把规范文件变成一个C程序,根据规范提供的输入来解析。转换规范到parser解析器的算法是复杂的,在这里不会被讨论(可以看引用的更多信息)。然而,parser本身相对简单,明白它怎么工作的,不是非常严格的必要,不过也要对待错误恢复和使歧义的地方更加可理解。
yacc生产的parser解析器包含使用一个栈的有限的状态机。解析器parser也读取和脊柱下一次输入的token(叫做先行token)。当前的状态总是在栈顶的那个元素。有限状态机的状态是小integer标签;初始化时,机器是在状态0,栈只包含一个状态0,没有先行token被读取到。
机器只有四个动作可用,叫做shift(移位),reduce(减少),accept(接受),还有error(出错)。解析器的动作按这几步完成:
-
根据当前状态,解析器parser决定它是否需要一个先行(lookahead)token来决定什么动作应该被完成;如果它需要1,但是没有1,它调用yylex来获取下一个token。
-
使用当前状态,如果需要先行token,解析器依赖它的下一个动作,然后执行官。这样的结果是状态被推到栈上,或者出栈,然后先行token被执行或者不管。
shift(移位)操作是解析器最常见的操作。任何时候只要有shift操作,总是有一个先行token。例如,在状态56可能有一个操作:
IF shift 34
这是在说,在状态56,如果先行token是IF,当前状态56被推到栈底,然后状态34成为当前状态(在栈顶)。最后先行token被清除。
reduce(减少)操作可以防止栈无限制地增长。减少操作是恰当的,当解析器已经看到右手边的语法规则,然后准备好了去宣布它已经看到了规则的一个实例,把右手边用左手边来替代。查阅先行token来决定是否减少可能是必要的,但是通常它没有;实际上,默认操作(由"."代表)经常是一个reduce操作。
reduce动作跟独特的语法规则有关。语法规则也是被赋予了小整形数字,导致了一些困惑。这个动作关联了语法规则18,
. reduce 18
同时这个动作操作关联了状态34
IF shift 34
假设被reduce的规则是A: x y z ;
reduce操作依赖左手边的标识(在这里是A),还有右手边的标识的数量(这里是3)。
为了reduce,首先从栈中出栈栈顶的三个状态(通常,状态数量出栈跟规则右侧的的标识数量一致)事实上,这些状态是识别到x,y,z的时候入栈的那些,并且不再提供任何有用的用途。这些状态出栈后,有一个状态没有被覆盖,是在解析器未开始进入处理规则之前的状态。使用这个未覆盖的状态,规则左侧的标识,执行A的移位操作的影响。一个新的状态被获得了,被推入栈,然后继续解析。处理左手边的标识和一个token的一个普通移位之间是有重大区别的,无论如何,这个操作被称作是一个goto操作。未覆盖的状态包含一个入口,例如:
A goto 20
导致状态20被推入栈,然后成为当前的状态。
实际上,reduce操作在解析中"时钟回拨",把状态出栈,回到规则的右手边被第一次解析器看到的状态。解析器然后表现得就像它当时已经看过左侧的样子。如果规则的右手边是空的,没有状态被出栈,为被覆盖的状态实际上是当前状态。
reduce操作在用户提供的动作和值的处理上也很重要。当一个规则被reduce处理,在栈被调整前,提供给规则的代码会被执行。除了栈持有状态,另一个并行运行的栈,持有了来自词法分析器和操作动作返回的值。从用户代码返回后,reduce操作被执行了。当goto操作被执行完,外部变量yyval被复制进value值栈。伪变量2等等,指向了值栈。
另外两个解析器操作动作在概念上更简单。接受动作表明整个输入已经被看见并且它匹配了规范。这个动作只出现在当先行token是结束标识符的时候,并且表明解析器已经成功地完成它的工作。报错动作,从另一方面来说,代表解析器无法再继续根据规范解析的一个地方。输入token它已经看到,连同先行token一起,无法被任何结果是正确的合法输入来跟踪了。解析器报告一个错误,试图去恢复这种情况并重新开始解析:错误恢复(与错误探测相反)会在第七章讲到
是时候来一个例子了!考虑这个规范:
%token DING DONG DELL
%%
rhyme : sound place
;
sound : DING DONG
;
place : DELL
;
当yacc被调用到-v的选项,一个被称为y.output的文件被产生,还有一个人类可读的解析器的描述。y.output文件与上面的语法相一致(最后去除了一些统计数据),文件是:
state 0
$accept : _rhyme $end
DING shift 3
. error
rhyme goto 1
sound goto 2
state 1
$accept : rhyme_$end
$end accept
. error
state 2
rhyme : sound_place
DELL shift 5
. error
place goto 4
state 3
sound : DING_DONG
DONG shift 6
. error
state 4
rhyme : sound place_ (1)
. reduce 1
state 5
place : DELL_ (3)
. reduce 3
state 6
sound : DING DONG_ (2)
. reduce 2
注意,除了每一个状态的动作,每一个状态中被解析出来处理的规则有一个描述。下划线_字符用来表明在每个规则中,什么是已经被看到的,还有什么是即将被看到的。假设输入是:
DING DONG DELL
跟着这些解析器处理输入的步骤是有益的。
首先,当前状态是state 0。解析器需要涉及到输入,为了决定动作actions在状态 0之间可用,所以第一个token,DING,读取了,成为先行token。DING在状态0的动作是"shift 3",所以状态3被推进栈顶,然后先行token被清除。状态3变成当前的状态。下一个token,DONG,读取了,成为先行token。token DONG上的状态3的动作是"shift 6",所以状态6入栈,然后先行token被清除。现在栈中包含状态0,3,6。在状态6,甚至没有考虑先行,解析器由规则2来reduce。
sound : DING DONG
这条规则在右手边有两个标识,所以两个状态6和3被出栈,没覆盖状态0。考虑到状态0的描述,在sound上寻找到一个goto,因此状态2入栈,成为当前状态。
sound goto 2
在状态2,下一个token是DELL,必须被读取到。这个动作是shift 5,所以状态5被推入栈,目前栈中有0,2,5,然后先行token被清除。在状态5,唯一的动作是由规则3执行reduce。这条规则有一个标识在右手边,所以状态5出栈,状态2没有被覆盖到。place上的状态2的goto,规则3的左侧,是状态4。现在栈包含了0,2,4.在状态4,唯一的动作是被规则1执行reduce。规则1右侧有两个标识,所以栈顶两个状态出栈,再次没覆盖到状态0。在状态0,有一个goto在rhyme,导致解析器进入状态1。在状态1,输入被读取;结束标记被获取,这是由y.output文件中的"$end"符号表明的。当结束标记被看到接收到的时候,状态1的动作是成功地结束了解析。
读者被敦促来思考一下当面临这种不正确字符串,像"DING DONG DONG,DING DONG,DING DONG DELL DELL等"时,解析器会怎样工作?当问题出现在更多复杂上下文时,花几分钟的时候思考和其他简单例子可能会得到回报。
未完待续,今天就先到这里,预告一下,下次会更新第六节语法树解析的《优先级》,敬请期待。