GNU Bison Parser 深入分析
一些约定
bison对我们定义好的文法进行了增广(augmentation),添加了$accept
和$end
符号表示接收和终止,并且增加了一条规则用于判断是否完成语法分析:
1 | $accept : <start-symbol> $end |
除此之外还增加了$undefined
来表示未出现在文法中的symbol。增加了error
用来生成错误。
一个简单的例子
就像flex一样,bison也是表驱动的,为了理解bison如何parse,我们用这样一个文法去生成一个parser:
1 | (1) L → L;E |
对应到bison里,就是这样:
1 | %% |
下面是这个文法的LALR(1)算法生成的自动机表(增广后的):
action | GOTO | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
state | ; | , | a | ( | ) | $end | L | E | P | M | |
0 | s1 | s2 | 3 | 4 | 5 | ||||||
1 | r5 | r5 | r5 | r5 | r5 | r5 | |||||
2 | r7 | r7 | s1 | s2 | r7 | r7 | 6 | 4 | 5 | 7 | |
3 | s9 | s8 | |||||||||
4 | r2 | s10 | r2 | r2 | r2 | r2 | |||||
5 | r4 | r4 | r4 | r4 | r4 | r4 | |||||
6 | s9 | r8 | r8 | r8 | r8 | r8 | |||||
7 | s11 | ||||||||||
8* | acc | acc | acc | acc | acc | acc | |||||
9 | s1 | s2 | 12 | 5 | |||||||
10 | s1 | s2 | s11 | 13 | |||||||
11 | r6 | r6 | r6 | r6 | r6 | r6 | |||||
12 | r1 | s10 | r1 | r1 | r1 | r1 | |||||
13 | r3 | r3 | r3 | r3 | r3 | r3 |
这个例子的符号表如下:
Symbol | Number |
---|---|
$end | 0 |
error | 1 |
$undefined | 2 |
';' | 3 |
',' | 4 |
'a' | 5 |
'(' | 6 |
')' | 7 |
$accept | 8 |
L | 9 |
E | 10 |
P | 11 |
M | 12 |
注意,$end
,error
, $undefined
对应的number永远是0,1,2,然后是terminal symbol,再然后是$accept
,再然后是non-terminal。
如此简单的语法都要生成一个庞大的表,更何况一门完备的语言,并且注意到了表中有很多地方是空的(对应error),因此bison对这个表进行了压缩。
数据压缩
default action
首先是对action的部分进行压缩,注意到state 1,2,4,5,6,11,12,13仅仅只有1种reduction操作(有的还有shift,但它不是reduction),因此就可以先单独考量这样的行,对它们定义一个default action(reduction),也就是只要碰到这些state,在排除shift操作后一律使用reduction操作。对于空白的error部分即使进行了reduction,也仅仅是推迟了错误的发生。
形式上这个就应该是这个样子:
1 | default_reductions[] = {none,r5,r7,none,r2,r4,r8,none,none,none,none,r6,r1,r3} |
在bison生成的文件种就对应了yydefact[]
,这个表的index表示的是state number,里面的值表示reduction的rule number,0表示error:
1 | static const yytype_uint8 yydefact[] ={0,6,8,0,3,5,9,0,1,0,0,7,2,4}; |
注意第一条规则是bison的默认增广规则,因此所有规则号都加了1。
default GOTO
接下来要压缩GOTO的部分,bison的压缩策略是这样的:将每一个non-terminal对应的GOTO列中最多的那一个单独拿出来(L:3,E:4,P:5,M:7)构成一个default GOTO:
1 | default_gotos[] = { 3, 4, 5, 7 } |
在生成文件中就对应了yydefgoto[]
:
1 | static const yytype_int8 yydefgoto[] ={ -1,3,4,5,7 }; |
它的index是用non-terminal的symbol number减去terminal个数得到的(0处对应的-1好像没有什么用)。
压缩non-default action
在选出default reduction表之后,去除它们后action剩下的部分是:
action | ||||||
---|---|---|---|---|---|---|
state | 3 | 4 | 5 | 6 | 7 | 0 |
0 | 1 | 2 | ||||
2 | 1 | 2 | ||||
3 | 9 | 8 | ||||
4 | 10 | |||||
6 | 9 | |||||
7 | 11 | |||||
9 | 1 | 2 | ||||
10 | 1 | 2 | 11 | |||
12 | 10 |
可以看出有很多空白,接下来的操作就是要对这些空白进行压缩。
bison会将这个表变成两个一维的表,一个用来存储这些非空白的信息,一个用来存储它们的dimension。一般表达式就是:
1 | action number = T[D[state number] + symbol number] |
例如,我们想要得到state 4在遇到symbol号为4时的操作,那就像这样进行索引:
1 | T[D[4] + 4] = 10 |
根据这个压缩规则,bison分别在yytable[]
和yypact[]
存储这个‘T’和‘D’:
1 | static const yytype_uint8 yytable[] ={8,1,2,9,11,10,9,6,12,0,0,0,13};static const yytype_int8 yypact[] ={-4,-5,-4,0,1,-5,3,-3,-5,-4,-4,-5,1,-5}; |
我们可以验证一下,state 4遇到规则4时对应的是s10:
1 | yytable[yypact[4]+4] = yytable[5] = 10 |
注意,尽管去除了default reduction,这里面的操作也是可能有reduction的,为此,bison使用正负号来区别它们,yytable[]
里正号代表shift,负号代表reduction(当然这个例子里没有),如果是0就执行default action。
压缩non-default GOTO
和non-default action一样,采用相同的方法去压缩non-default GOTO。只是在indexing的时候,公式变为了:
1 | action number = T[ND[symbol number - num_terminal] + state number] |
在这个例子中,non-default GOTO的表是这样的:
GOTO | |||
---|---|---|---|
state | 9 | 10 | 11 |
2 | 6 | ||
9 | 12 | ||
10 | 13 |
压缩后的index在bison生成的表中对应了yypgoto[]
,value仍然用yytable[]
:
1 | static const yytype_int8 yypgoto[] ={ -5, 5, -1, 2, -5}; |
例如,我们要symbol 9在state 2中的GOTO:
1 | yytable[yypgoto[9-8]+2] = yytable[7] = 6 |
算法的实现
一些static数据
yytranslate
yytranslate[]
定义了文法中符号到symbol number的映射过程,假如这些符号是non-terminal,那index就对应non-terminal %token声明时的值,如果这些符号是ASCII码中的terminal,那么index就对应了ASCII值。
1 | static const yytype_uint8 yytranslate[] ={ 0, 2, 2, 2, 2, ...}; |
比如说这个例子中出现的’a’,对应的ASCII值是97,根据yytranslate[]
的索引,可以得到符号a对应的symbol number是5:
1 | yytranslate[97] = 5; |
在这里面有很多的2(undefined),因为很多ASCII表中的符号并没有出现在文法中。
yyr1
yyr1[]
是每条文法规则的LHS符号的symbol number:
1 | static const yytype_uint8 yyr1[] ={ 0, 8, 9, 9, 10, 10, 11, 11, 12, 12}; |
0并没有用作文法的number,所以yyr1[0]
是0.
yyr2
yyr2[]
是每条文法规则的RHS符号的个数:
1 | static const yytype_uint8 yyr2[] ={ 0, 2, 3, 1, 3, 1, 1, 3, 0, 1}; |
同上,yyr2[0]`是0.
yycheck
yycheck[]
是用来判断使用default还是non-default的,在生成文件的代码中表现为:
1 | /* If the proper action on seeing token YYTOKEN is to reduce or to detect an error, take that action. */ |
在这个例子中,yycheck[]
的值如下:
1 | static const yytype_int8 yycheck[] ={ 0, 5, 6, 3, 7, 4, 3, 2, 9, -1, -1, -1, 10}; |
其他不重要的表:
1 | yyrhs: yyrhs[n] = 第n条规则的RHS第一个symbol |
yyparse()
整个算法的主程序是yyparse(),具体的框架如下:
首先我们要定义一个state stack:
1 | /* The state stack. */ |
然后定义符号的stack:
1 | /* The semantic value stack. */ |
这里的YYSTYPE
是文法的non-terminal所允许的type,可以自己定义,在这个例子里默认为了int
。
然后定义用来输出错误的一个buffer:
1 | /* Buffer for error messages, and its allocated size. */ |
定义将new state push到栈里的操作:
1 | yynewstate: |
核心部分只是前面和后面的几行代码,中间都是在处理overflow的,用一些宏来决定是将stack size 扩张两倍还是直接扔出memory exhausted错误。
然后定义读取lookahead token的操作yybackup,这里就要决定是使用default还是non-default,假如是non-default,是shift还是reduce还是报错了:
1 | yybackup: |
然后定义default操作,很简单,读取yydefault[]表,如果是0就报错,不是0就进入reduce:
1 | yydefault: |
接下来是reduce操作:根据yyr2[]找到对应规则的reduce的RHS符号个数以用来决定pop多少,然后根据yyr1[]找到对应规则的LHS值push进去:
1 | yyreduce: |
然后是三个用来检测error的操作:
1 | yyerrlab: |
然后定义accept,abort,overflow时yyparse()分别返回什么值:
1 | yyacceptlab: |
最后一步:yyreturn,返回时清空stack,error message的buf:
1 | yyreturn: |