跟我学Lex and Yacc 二

  读了寒蝉退士翻译的<<Yacc: 另一个编译器的编译器>>中解析器是如何工作的这段后,很有感触,最近在学习中也碰到了一些关于如何归约的问题,很是困惑,看了这篇文章后,有很大收获。
  在程序设计的时候,经常有这样嵌套语法:
  A: b C
    ;
  b: d
   | f g
   | h
    ;
  d: int
    | float;
    | char
    | BYTE
 
假设当词法分析器Lex匹配了int ,根据上面的规则,移进int的时候,规约成了d,d成为当前状态,那么d会不会直接规约成b,或者在什么情况下会直接规约为b呢。这就是我的困惑,可能也是很多初学者的疑惑,因为根本不了解解析器是如何工作的,就直观的从语法上去思索,其实这些都是一些自己的凭空想法,没有一点依据。当熟悉了解析器是如何工作的后,这些就都不是困难了。

分析下面的语法:

%token  DING  DONG  DELL  LING GOOD
  
%%
      rhyme   :       sound   place {}
      sound   :       a
                     |       b
                     ;
            a       :      DING DONG
                     ;
           b        :       GOOD
                     ;
       place    :       DELL          {}
       ;
%%
语法分析器通过Bison产生的输出文件的内容具体为:


Terminals which are not used:

   LING

Grammar
rule 1    rhyme -> sound place
rule 2    sound -> a
rule 3    sound -> b
rule 4    a -> DING DONG
rule 5    b -> GOOD
rule 6    place -> DELL

Terminals, with rules where they appear

$ (-1)
error (256)
DING (258) 4
DONG (259) 4
DELL (260) 6
LING (261)
GOOD (262) 5

Nonterminals, with rules where they appear

rhyme (8)
    on left: 1
sound (9)
    on left: 2 3, on right: 1
a (10)
    on left: 4, on right: 2
b (11)
    on left: 5, on right: 3
place (12)
    on left: 6, on right: 1
state 0

    DING shift, and go to state 1
    GOOD shift, and go to state 2

    rhyme go to state 9
    sound go to state 3
    a    go to state 4
    b    go to state 5
state 1

    a  ->  DING . DONG   (rule 4)

    DONG shift, and go to state 6
state 2

    b  ->  GOOD .   (rule 5)

    $default reduce using rule 5 (b)
state 3

    rhyme  ->  sound . place   (rule 1)

    DELL shift, and go to state 7

    place go to state 8
state 4

    sound  ->  a .   (rule 2)

    $default reduce using rule 2 (sound)
state 5

    sound  ->  b .   (rule 3)

    $default reduce using rule 3 (sound)
state 6

    a  ->  DING DONG .   (rule 4)

    $default reduce using rule 4 (a)
state 7

    place  ->  DELL .   (rule 6)

    $default reduce using rule 6 (place)
state 8

    rhyme  ->  sound place .   (rule 1)

    $default reduce using rule 1 (rhyme)
state 9

    $    go to state 10
state 10

    $    go to state 11
state 11

    $default accept


在上面的语法规则中,当通过state 6 规约为a后,此时state 0成为超前记号,此时将跳转到state 4,state 4成为超前记号,state 4有将规

约为sound,因此state 0又成为超前记号,此时将跳转到state 3,等到输入DELL,如果下一个记号不是DELL,将会出现错误,错误信息为: parse

error, expecting 'DELL'.在上面的例子中,当归约成a后,主动规约成了sound.

在来看下面的语法规则
%token  DING  DONG  DELL  LING GOOD HELLO
  
%%
      rhyme   :       sound   place {}
      sound   :       a
                     |       b
                     ;
           a        :      DING DONG
                      |      a HELLO           /*........这个是我们后加入的......*/
                      ;
           b        :       GOOD
                     ;
       place    :       DELL          {}
       ;
%%

此时的状态输出文件如下:


Terminals which are not used:

   LING

Grammar
rule 1    rhyme -> sound place
rule 2    sound -> a
rule 3    sound -> b
rule 4    a -> DING DONG
rule 5    a -> a HELLO
rule 6    b -> GOOD
rule 7    place -> DELL

Terminals, with rules where they appear

$ (-1)
error (256)
DING (258) 4
DONG (259) 4
DELL (260) 7
LING (261)
GOOD (262) 6
HELLO (263) 5

Nonterminals, with rules where they appear

rhyme (9)
    on left: 1
sound (10)
    on left: 2 3, on right: 1
a (11)
    on left: 4 5, on right: 2 5
b (12)
    on left: 6, on right: 3
place (13)
    on left: 7, on right: 1


state 0

    DING shift, and go to state 1
    GOOD shift, and go to state 2

    rhyme go to state 10
    sound go to state 3
    a    go to state 4
    b    go to state 5
state 1

    a  ->  DING . DONG   (rule 4)

    DONG shift, and go to state 6
state 2

    b  ->  GOOD .   (rule 6)

    $default reduce using rule 6 (b)
state 3

    rhyme  ->  sound . place   (rule 1)

    DELL shift, and go to state 7

    place go to state 8
state 4

    sound  ->  a .   (rule 2)
    a  ->  a . HELLO   (rule 5)

    HELLO shift, and go to state 9

    $default reduce using rule 2 (sound)
state 5

    sound  ->  b .   (rule 3)

    $default reduce using rule 3 (sound)
state 6

    a  ->  DING DONG .   (rule 4)

    $default reduce using rule 4 (a)
state 7

    place  ->  DELL .   (rule 7)

    $default reduce using rule 7 (place)
state 8

    rhyme  ->  sound place .   (rule 1)

    $default reduce using rule 1 (rhyme)
state 9

    a  ->  a HELLO .   (rule 5)

    $default reduce using rule 5 (a)
state 10

    $    go to state 11
state 11

    $    go to state 12
state 12

    $default accept对照

现在来分析当前的语法:假设当前的状态为state 6,当在state 6归约为a后,此时state 0为暴露状态,查找对a的跳转,a go to state 4,此

时state 4成为当前记号,在state 4中必须读取下一个hello记号。并没有直接主动归约为sound,它应该查看下一个超前记号是什么,如果是

HELLO,切换到state 9 ,默认归约后,又继续回到当前状态state 4 ,如果不是HELLO,则默认归约,归约后当前状态变为state 0,查找对sound

的跳转,跳转到了state 3,在state 3接受下一个记号DELL,如果不是DELL,将出现语法分析错误:parse error, expecting 'DELL'.
 

附上寒蝉退士翻译的解析器是如何工作的片段:解析器如何工作

Yacc 把规定文件转换成 C 程序,它依据给出的规定解析输入。做从规定到解析器转换的算法是复杂的,就不在这里讨论了(更多信息参见引用)。但是,解析器自身就相对简单了,理解它是如何工作的,尽管不是严格必须的,但会使错误修复和歧义处置更加易于理解。

Yacc 提供的解析器是由带有一个栈的有穷状态自动机组成。解析器自身还有能力读取和记住(叫做超前(lookahead)记号)下一个输入记号。当前状态总是在栈顶。有穷状态自动机的状态是一个给定的小整数标签(label);最初时,机器是在状态 0 下,栈只包含状态 0,没有读取超前记号。

机器对它只能获得四个动作,叫做移进(shift)、归约(reduce)、接受和错误。解析器的移动按如下规则进行:

1. 基于它的当前状态,解析器决定是否需要一个超前记号来决定应当做什么动作;如果需要并且没有读取,则调用 yylex 来获得下一个记号。

2. 使用当前状态,和超前记号(如果需要的话),解析器决定它的下一个状态,并完成它。这可能导致状态压入栈中,或从栈中弹出来,和导致超前记号被处理或保留。

移进动作是解析器做的最常见的动作。在做移进动作的时候,这里总是有一个超前记号。例如,在状态 56 下有这么一个动作:

                IF      shift 34

这是说,在状态 56 下,如果超前字符是 IF,则当前状态(56)在栈中被压下去,而状态 34 成为当前状态(在栈顶)。超前字符被清除。
归约动作防止栈无限制的增长。在解析器已经见到一个文法的右手端的时候做归约动作是适当的,它准备好宣布它已经见到这个规则的一个实例(instance),用这个规则的左手端替换它的右手端。有可能需要参考超前记号来决定是否归约,但是通常不需要;实际上,缺省动作(表示为“.”)经常是一个归约动作。

归约动作与单独的文法规则相关联。文法规则也以小整数给出,这导致了一些混淆。动作

        .       reduce 18

提及的是文法规则 18,而动作
        IF      shift 34

提及的是状态 34。
假定要归约的规则是

        A : x y z ;
归约动作依赖于左手端符号(symbol)(这里是 A),和右手端符号的数目(这里是 3)。要归约,首先从栈顶中弹出三个状态(一般的,弹出的状态数目等于规则右手端符号的数目)。在效果上,这些状态识在识别 x、y 和 z 的时候压入栈中的,并不再有任何用处。在弹出这些状态之后,开始处理这个规则之前,分析器处在暴露(uncovered)状态下。使用这个暴露状态,和在规则左手端的符号,进行实效上的移进 A。获得一个新状态,压入栈中,并继续分析。在处理左手端符号和记号的普通移进之间有一个重要的区别,所以这个动作叫做跳转(goto)动作。特别是,移进清除超前记号,而跳转不影响它。在任何情况下,暴露状态都包含一个条目比如:

        A       goto 20

导致状态 20 被压入栈中,并成为当前状态。
在效果上,归约动作在解析器中“把钟拨回”,从栈中弹出状态,以此回到首次见到这个规则的右手端的那个状态。解析器接着运转,如同它已经在此时见到了左手端那样。如果这个规则的右手端为空,则不从栈中弹出状态: 暴露状态实际上就是当前状态。

归约动作在用户提供的动作和值的处置中也是很重要的。在一个规则被归约的时候,在调整栈之前执行这个规则提供的代码。除了持有状态栈之外,还有另一个栈与它并行运行,它持有从词法分析器和这些动作返回的值。在发生移进的时候,把外部变量 yylval 复制到值栈顶上。在从用户代码返回之后,完成归约。在做跳转动作的时候,把外部变量 yyval 复制到到值栈顶上。伪变量 $1、$2 等提及的就是这个值栈。

其他两个解析器动作在概念上非常简单。接受动作指示整个输入已经查看完了并且它与规定相匹配。这个动作只在超前记号是结束标记的时候出现,并指示出解析器已经成功的完成了它的工作。在另一方面,错误动作表示解析器不能再继续依据规定做解析的状况。已经见到的输入记号,与超前记号一起,不能遵循导致合法输入的任何东西。解析器报告一个错误,并尝试恢复状态并重新开始解析: 错误修复(相对于错误检测)将在第 7 节中叙述。

是给出例子的时候了! 考虑下列规定

        %token  DING  DONG  DELL
        %%
        rhyme   :       sound  place
                ;
        sound   :       DING  DONG
                ;
        place   :       DELL
                ;

在使用 -v 选项调用 Yacc 的时候,生成一个叫做 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

跟踪解析器在处理这个输入期间的步骤是有教益的。
最初,当前状态是 0。解析器需要参照输入来在状态 0 下能获得的动作中做出抉择,所以读入了第一个记号 DING,它成为超前记号。在状态 0 下对记号 DING 的动作是“shift 3”,所以状态 3 被压入栈中,超前记号被清除。状态 3 成为当前状态。读入下一个记号 DONG,它成为超前记号。在状态 3 下对记号 DONG 的动作是“shift 6”所以状态 6 被压入栈中,超前记号被清除。栈现在包含 0、3 和 6。在 状态 6 下,不用参考超前记号,解析器按规则 2 归约。

        sound  :   DING  DONG

这个规则在右手端有两个符号。所以从栈中弹出两个状态 6 和 3,暴露了状态 0。参照状态 0 的描述,查找对 sound 的跳转,获得了
        sound   goto 2

;所以状态 2 压入中,成为当前状态。
在状态 2 下,必须读取下一个记号 DELL。动作是“shift 5”,所以状态 5 被压入栈中,栈中现在有 0、2 和 5,超前记号被清除。在状态 5 下,唯一的动作是按规则 3 归约。它在右手端有一个符号,所以从栈中弹出一个状态 5,暴露了状态 2。在状态 2 下对规则 3 的左手端 place 做跳转到状态 4。现在栈包含 0、2 和 4。在状态 4 下,唯一的动作是按规则 1 归约。规则 1 右手端有两个符号,所以从栈中弹出两个状态,再次暴露了状态 0。在状态 0 下,对 rhyme 有一个跳转导致分析器进入状态 1。在状态 1 下,读取输入,获得了结束标记,它在 y.output 中用“$end”来指示。在状态 1 下在见到结束标记时的动作是接受,成功的结束了解析。

读者可能急切的想知道在面对不正确的字符串比如 DING DONG DONG、DING DONG、DING DONG DELL DELL 等的时候解析器如何工作。在这个和其他简单例子中多花点时间,在更复杂的上下文中出现问题的时候解决起来就快速了。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值