ps:补上了图
实验要求
-
了解 Bison 基础知识和理解 Cminus-f 语法(重在了解如何将⽂法产⽣式转换为 Bison 语句)
-
阅读 /src/common/SyntaxTree.c ,对应头⽂件 /include/SyntaxTree.h (重在理解分析树如何⽣成)
-
了解 Bison 与 Flex 之间是如何协同⼯作,看懂 pass_node 函数并改写 Lab1 代码(提示:了
解 yylval 是如何⼯作,在代码层⾯上如何将值传给 $1 、 $2 等)
- 补全 src/parser/syntax_analyzer.y ⽂件和 lexical_analyzer.l ⽂件 ,完成语法分析器,要求最终能够输出解析树。
实验设计
该实验需要完成lexical_analyzer.l和syntax_analyzer.y这两个文件的设计,首先设计.y文件:每个 Bison 文件由序言,Bison 声明,语法规则,结尾这几部分组成,每个部分由 %%
分隔。
序言(用%{ %}括起来)
我在序言里进行了头文件的声明,其中syntax_tree.h定义了结点的结构体_syntax_tree_node和 树的结构体_syntax_tree。还声明了建树的相关函数,如new_syntax_tree_node为在树中新建节点,syntax_tree_add_child为添加子节点,new_syntax_tree为建树,del_syntax_tree为删除树,print_syntax_tree为打印树;
序言中还有外部函数和变量的声明,其中yylex()是FLEX工具按照定义好的规则自动生成的C函数,这个C函数把文本串作为输入,按照定义好的规则分析文本串中的字符,找到符合规则的一些字符序列后,就执行在规则中定义好的动作(Action)。找到符合规则的一些字符序列后用yytext指向字符序列的首字符。yyin表示指定的文件,yylex()函数从该文件读取字符:
我在序言中还定义了需要使用的变量和函数声明,如gt变量表示生成的解析树的根节点,yyerror函数处理yacc语法分析程序探测到的语法错误,node函数生成一个结点和它的子节点,并将该结点加入到树中:
Bison 声明
该部分是声明终结符和非终结符,和优先级的定义:
声明终结符:
用%token进行声明,其方法为:
%token<语义值类型> name
Bison会在分析器中将这个声明转换成#define
指令 以便yylex
用name代表这个记号类型码。当栈类型是一个联合体的时候, 就需要指明记号的语义值类型。在实验中我将语义值类型定义为node,该类型通过%union进行声明:
这是我声明的非终结符,这些名称和lab1中的token名称对应:
声明非终结符:
用% type为非终结符指定语义值类型,其类型在%union
中给定,方法如下:
%type<语义值类型> nonterminal
这是我声明的终结符,这些名称参考lab2中Cminus-f 的语法规则:
声明开始符号:
使用%start
进行声明:%start program
优先级和结合性的定义:
当记号的优先级相同时, 如何嵌套使用它们取决于它们的结合性. 当一个记号的优先级更高时,它将先被组合.为了解决LALR中的冲突问题,需要确定好各符号的优先级和结合性。通过查阅bison使用手册,知道怎么设定优先级和结合性:
先声明的优先级低,后声明的优先级高
%left:指明有左结合性
%right:指明有右结合性
%nonassoc:指明没有结合性
我设定了如下优先级和结合性:
运算符 | 结合性 | 优先级(数字越大优先级越低) |
---|---|---|
[] | 左结合 | 1 |
() | 左结合 | 1 |
/ | 左结合 | 2 |
* | 左结合 | 2 |
+ | 左结合 | 3 |
- | 左结合 | 3 |
> | 左结合 | 4 |
>= | 左结合 | 4 |
< | 左结合 | 4 |
<= | 左结合 | 4 |
== | 左结合 | 5 |
!= | 左结合 | 5 |
= | 右结合 | 6 |
, | 左结合 | 7 |
语法规则
通过查阅Bison手册可知,一个语法规则的形式如下:
result: components1{ }
| components2{ }
;
- result为这个规则所描述的非终结符;components 为被这个规则组合在一起的多种终结符和非终结符;
- 用“|”将多条规则进行连接 ; “;”表示规则描述结束;
- 花括号{}里为这个规则的实例被识别出来后进行的动作,一般动作用于计算规则左端的语义值**$ ∗ ∗ ,动作中可以使用 ∗ ∗ **,动作中可以使用** ∗∗,动作中可以使用∗∗n来引用**规则中各部件的语义值,此时$n有对应非终结符或终结符的数据类型
根据上述,我编写的规则如下(由于规则太多这里只显示部分):
规则1描述了非终结符program,该动作里面有两条语句:
语句1:
- 使用了**$1引用部件declaration-list的语义值**,由于在之前声明该非终结符的语义值类型为node,所以$1为node型。
- 它调用函数node创建结点,依次传入参数:结点名称,结点的子节点个数,子节点的语义值(即$1),创建好后返回syntax_tree_node *类型,它指向当前创建的结点,将返回值赋给$$,得到规则左边的语义值;
- 对于其它规则的动作,里面的语句基本上和语句1差不多;
语句2:
由于program为开始符号,当识别到该规则的实例,表示这棵树已经建完,此时需要获得该树,而语句2就是实现该操作:将规则左边的语义值,即树的根节点赋给gt->root,打印树时使用变量gt就可以了。
结尾
结尾部分实现了三个函数parse,node和yyerror:
parse函数:
- 首先它判断input_path是否为NULL,如果是则从 stdin 读取,如果不是就从文件中读取;
- 然后对变量lines,pos_start,pos_end,gt作初始化;
- 然后使用yyrestart(yyin);,因为每次调用yypars,它会忘记上次分析可能拥有的任何状态而重新开始分析,而每次调用yylex它都从上次离开的地方继续分析。所以要在每次parse之前调用yyrestart()来确保lex从头开始分析而不是从上次的地方继续分析。
- 然后调用函数yyparse开始进行分析. 这个函数读入记号,执行动作, 并且最后如果它遇到输入结束或者不能恢复的错误就会返回.
- 最后返回树
node函数为构造树节点,当该节点的孩子个数为0时,给该节点添加一个空字符子节点,否则按顺序添加传入的子节点:
yyerror函数:每当yyparse发现一个语法错误的时候, yyparse就会调用该函数进行报错,这里打印报错的哪行哪列信息
完成lexical_analyzer.l文件
将实验1的 .l文件复制到这个文件当中,并做了以下改动:
- 去掉辅助函数部分;
- 识别到规则时加上将值放入Bison栈中和返回记号的类型的操作;
如下为修改后的操作, 通过pass_node函数将记号的值放入栈中,然后再用return将记号的类型返回:
pass_node函数已经给出:
它传入的是捕获到的字符串text,将text又作为参数传入创建结点函数中,然后将创建好的名为text的节点(这是记号的语义值)存放在全局变量yylval,当存储类型为node的语义值时, 需要使用恰当的联合体成员,所以是yylval.node来存的。这一操作实际上就是将void这个非终结符的记号值存放到node类型的栈中;
不过识别到EOL, BLANK, COMMENT,ERROR时不需要进行压栈(它们不做非终结符)和返回记号类型,它们只需要更改记录位置的变量:
实验结果验证
可以通过如下命令进行编译:
bison –d lexical_analyzer.y //生成syntax_analyzer.c,syntax_analyzer.h(包含.y中定义的所有终结符的记号类型,记号值从258开始;栈的类型定义;变量yylval的外部引用定义
flex lexical_analyzer.l //编译.l得到lex.yy.c,lex.yy.c中包含了函数yylex()
gcc syntax_analyzer.c lex.yy.c
也可以使用make parser,通过写好的Makefile文件进行编译。
编译好后输入命令进行验证:
./tests/lab2/test_syntax.sh easy
./tests/lab2/test_syntax.sh normal
diff ./tests/lab2/syntree_easy ./tests/lab2/syntree_easy_std
diff ./tests/lab2/syntree_normal ./tests/lab2/syntree_normal_std
可以看到diff后没有其他输出(ifs为我自己写的测试样例),表示通过给出的 easy 测试集和通过 normal 测试集。
测试我自己写的ifs测试样例:
我写这个测试样例主要是为了观察在有if嵌套的情况下,生成的树是什么样的。因为在Bison使用手册中,我看到"悬挂else"歧义,本次实验的语法中也有这种现状存在:
当ELSE被读入成为超前扫描记号, 栈中的内容刚好可以由第一个规则进行归约. 但是移进ELSE也是合法的。此时会产生一个移进/归约冲突(这是可以预见的合法的移进/归约冲突, 可以使用%expect n
消除警告)。 Bison被设计成选择移进来解决这些冲突.因为已经建立的惯例是通过将else与最里面的if匹配来解决歧义;
测试结果分析:
对于第一个if模块,它的结构是 if { if – else},在树中对应为:
对于第二个if模块,它的结构是 if { if} – else,在树中对应为:
可以看到,由于stm中有{},所以这个语法分析器在分析IF stm IF stm ELSE stm结构时,会根据 { } 的范围决定遇到ELSE时是要进行归约(与外面的if匹配)还是移进(与最里面的if匹配)。
思考题
-
在1.3样例代码中存在左递归文法,为什么
bison
可以处理?(提示:不用研究bison
内部运作机制,在下面知识介绍中有提到bison
的一种属性,请结合课内知识思考)因为bison是**使用LALR(1)**将文法转为解析器的 ,LALR使用了前看符号(在归约时通过FOLLOW(N)选择性归约),所以通过前看符号可以解决左递归文法出现的冲突;
-
请在代码层面上简述下
yylval
是怎么完成协同工作的。(提示:无需研究原理,只分析维护了什么数据结构,该数据结构是怎么和$1
、$2
等联系起来?)flex通过正则表达式读到匹配的字符串后,将字符串转为对应非终结符的语义值,然后将这个语义值放在全局变量yylval中,yylval相当于一个栈,栈的类型可以由%union定义。Bison维护一个栈(这个栈中的每一个元素的值,都是由yylval所指定)来保存文法符号的语义值,当最后n个被移进的记号和语义值匹配某个语法规则时, 就将它们依次弹出栈,再将规则的左部压栈**(归约)**。
bison定义$ 和 和 和n来引用栈中的元素:****$$表示规则左部,即归约之后被重新压入栈中的元素;$n表示规则左边第n个部件的语义值,即归约之前栈中距离栈顶编号为i的元素;
-
请尝试使用1.3样例代码运行除法运算除数为0的例子(测试case中有)看下是否可以通过,为什么我们在case中把该例子认为是合法的?(请从语法与语义上简单思考)
可以通过;语法分析器认为除数为0是合法的,因为**“2/0”可以由上面规定的文法推导出来**,所以从语法上来说它是合法的,由于语法分析使用的是上下文无关文法,所以它不能判断语义是否合法;
- 能否尝试修改下1.3计算器文法,使得它支持除数0规避功能。
词法分析器在读到非终结符NUMBER时,先判断yytext获取到的值是否为0,不为0才将它的语义值压入到yylval.num中,否则不将其传到语法分析器中:
修改之后,若除数为0,则直接报错,支持除数0规避功能:
实验反馈
- 通过实现一个cminus-f语法分析器,我大致了解了bison的分析过程:
- 调用函数
yyparse
开始进行分析; - 用词法分析器读取记号:
yylex
从输入流中识别记号并将记号类型的正值数字码(数字码用来确定需要解析的token类型)返回给语法分析器(数字码在bison编译.y文件时生成的.h文件里),并将这些记号和它们的语义值压入栈中(移进); - 当最后n个被移进的记号和组匹配某个语法规则时, 可以由那个规则将它们结合起来(归约),这些记号被规则的左部取代。动作是处理归约的一部分, 因为动作会计算这个组的语意值;
- 当
yyparse
遇到输入结束或者不能恢复的错误就会返回;
- 调用函数