yacc语法树系列(四)

431 阅读6分钟

这是我参与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(出错)。解析器的动作按这几步完成:

  1. 根据当前状态,解析器parser决定它是否需要一个先行(lookahead)token来决定什么动作应该被完成;如果它需要1,但是没有1,它调用yylex来获取下一个token。

  2. 使用当前状态,如果需要先行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值栈。伪变量1,1,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等"时,解析器会怎样工作?当问题出现在更多复杂上下文时,花几分钟的时候思考和其他简单例子可能会得到回报。

未完待续,今天就先到这里,预告一下,下次会更新第六节语法树解析的《优先级》,敬请期待。