读了寒蝉退士翻译的<<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 等的时候解析器如何工作。在这个和其他简单例子中多花点时间,在更复杂的上下文中出现问题的时候解决起来就快速了。