LAB2实验报告文档
- 了解
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
文件
Tips:在未编译的代码文件中是无法看到关于协同工作部分的代码,建议先编译 1.3 给出的计算器样例代码,再阅读 /build/src/parser/
中的 syntax_analyzer.h
与 syntax_analyzer.c
文件
思考题
本部分不算做实验分,出题的本意在于想要帮助同学们加深对实验细节的理解,欢迎有兴趣和余力的同学在报告中写下你的思考答案,或者在issue中分享出你的看法。
-
在1.3样例代码中存在左递归文法,为什么
bison
可以处理?(提示:不用研究bison
内部运作机制,在下面知识介绍中有提到bison
的一种属性,请结合课内知识思考) -
请在代码层面上简述下
yylval
是怎么完成协同工作的。(提示:无需研究原理,只分析维护了什么数据结构,该数据结构是怎么和$1
、$2
等联系起来?) -
请尝试使用1.3样例代码运行除法运算除数为0的例子(测试case中有)看下是否可以通过,如果不,为什么我们在case中把该例子认为是合法的?(请从语法与语义上简单思考)
-
能否尝试修改下1.3计算器文法,使得它支持除数0规避功能。
1.基础知识
我们在这里简单介绍如何让 bison
和 flex
协同工作及其原理,并简单介绍 bison
的一些基础知识。
1.1 Cminus-f 语法
本小节将给出Cminus-f的语法,该语法在Cminus语言的基础上增加了float类型。
Cminus的详情请参考《编译原理与实践》第九章附录。
我们将 Cminus-f 的所有规则分为五类。
- 字面量、关键字、运算符与标识符
id
type-specifier
relop
addop
mulop
- 声明
declaration-list
declaration
var-declaration
fun-declaration
local-declarations
- 语句
compound-stmt
statement-list
statement
expression-stmt
iteration-stmt
selection-stmt
return-stmt
- 表达式
expression
var
additive-expression
term
factor
integer
float
call
- 其他
params
param-list
param
args
arg-list
起始符号是 program
。
Cminus-f语法
- ‘ program → declaration-list ‘ `\text{program} \rightarrow \text{declaration-list}` ‘program→declaration-list‘
- ‘ declaration-list → declaration-list declaration ∣ declaration ‘ `\text{declaration-list} \rightarrow \text{declaration-list}\ \text{declaration}\ |\ \text{declaration}` ‘declaration-list→declaration-list declaration ∣ declaration‘
- ‘ declaration → var-declaration ∣ fun-declaration ‘ `\text{declaration} \rightarrow \text{var-declaration}\ |\ \text{fun-declaration}` ‘declaration→var-declaration ∣ fun-declaration‘
- ‘ var-declaration → type-specifier ID ; ∣ type-specifier ID [ INTEGER ] ; ‘ `\text{var-declaration}\ \rightarrow \text{type-specifier}\ \textbf{ID}\ \textbf{;}\ |\ \text{type-specifier}\ \textbf{ID}\ \textbf{[}\ \textbf{INTEGER}\ \textbf{]}\ \textbf{;}` ‘var-declaration →type-specifier ID ; ∣ type-specifier ID [ INTEGER ] ;‘
- ‘ type-specifier → int ∣ float ∣ void ‘ `\text{type-specifier} \rightarrow \textbf{int}\ |\ \textbf{float}\ |\ \textbf{void}` ‘type-specifier→int ∣ float ∣ void‘
- ‘ fun-declaration → type-specifier ID ( params ) compound-stmt ‘ `\text{fun-declaration} \rightarrow \text{type-specifier}\ \textbf{ID}\ \textbf{(}\ \text{params}\ \textbf{)}\ \text{compound-stmt}` ‘fun-declaration→type-specifier ID ( params ) compound-stmt‘
- ‘ params → param-list ∣ void ‘ `\text{params} \rightarrow \text{param-list}\ |\ \textbf{void}` ‘params→param-list ∣ void‘
- ‘ param-list → param-list , param ∣ param ‘ `\text{param-list} \rightarrow \text{param-list}\ ,\ \text{param}\ |\ \text{param}` ‘param-list→param-list , param ∣ param‘
- ‘ param → type-specifier ID ∣ type-specifier ID [] ‘ `\text{param} \rightarrow \text{type-specifier}\ \textbf{ID}\ |\ \text{type-specifier}\ \textbf{ID}\ \textbf{[]}` ‘param→type-specifier ID ∣ type-specifier ID []‘
- ‘ compound-stmt → { local-declarations statement-list } ‘ `\text{compound-stmt} \rightarrow \textbf{\{}\ \text{local-declarations}\ \text{statement-list} \textbf{\}}` ‘compound-stmt→{ local-declarations statement-list}‘
- ‘ local-declarations → local-declarations var-declaration ∣ empty ‘ `\text{local-declarations} \rightarrow \text{local-declarations var-declaration}\ |\ \text{empty}` ‘local-declarations→local-declarations var-declaration ∣ empty‘
- ‘ statement-list → statement-list statement ∣ empty ‘ `\text{statement-list} \rightarrow \text{statement-list}\ \text{statement}\ |\ \text{empty}` ‘statement-list→statement-list statement ∣ empty‘
- ‘ statement → expression-stmt ∣ compound-stmt ∣ selection-stmt ∣ iteration-stmt ∣ return-stmt ‘ `\begin{aligned}\text{statement} \rightarrow\ &\text{expression-stmt}\\ &|\ \text{compound-stmt}\\ &|\ \text{selection-stmt}\\ &|\ \text{iteration-stmt}\\ &|\ \text{return-stmt}\end{aligned}` ‘statement→ expression-stmt∣ compound-stmt∣ selection-stmt∣ iteration-stmt∣ return-stmt‘
- ‘ expression-stmt → expression ; ∣ ; ‘ `\text{expression-stmt} \rightarrow \text{expression}\ \textbf{;}\ |\ \textbf{;}` ‘expression-stmt→expression ; ∣ ;‘
- ‘ selection-stmt → if ( expression ) statement ∣ if ( expression ) statement else statement ‘ `\begin{aligned}\text{selection-stmt} \rightarrow\ &\textbf{if}\ \textbf{(}\ \text{expression}\ \textbf{)}\ \text{statement}\\ &|\ \textbf{if}\ \textbf{(}\ \text{expression}\ \textbf{)}\ \text{statement}\ \textbf{else}\ \text{statement}\end{aligned}` ‘selection-stmt→ if ( expression ) statement∣ if ( expression ) statement else statement‘
- ‘ iteration-stmt → while ( expression ) statement ‘ `\text{iteration-stmt} \rightarrow \textbf{while}\ \textbf{(}\ \text{expression}\ \textbf{)}\ \text{statement}` ‘iteration-stmt→while ( expression ) statement‘
- ‘ return-stmt → return ; ∣ return expression ; ‘ `\text{return-stmt} \rightarrow \textbf{return}\ \textbf{;}\ |\ \textbf{return}\ \text{expression}\ \textbf{;}` ‘return-stmt→return ; ∣ return expression ;‘
- ‘ expression → var = expression ∣ simple-expression ‘ `\text{expression} \rightarrow \text{var}\ \textbf{=}\ \text{expression}\ |\ \text{simple-expression}` ‘expression→var = expression ∣ simple-expression‘
- ‘ var → ID ∣ ID [ expression ] ‘ `\text{var} \rightarrow \textbf{ID}\ |\ \textbf{ID}\ \textbf{[}\ \text{expression} \textbf{]}` ‘var→ID ∣ ID [ expression]‘
- ‘ simple-expression → additive-expression relop additive-expression ∣ additive-expression ‘ `\text{simple-expression} \rightarrow \text{additive-expression}\ \text{relop}\ \text{additive-expression}\ |\ \text{additive-expression}` ‘simple-expression→additive-expression relop additive-expression ∣ additive-expression‘
- ‘ relop → <= ∣ < ∣ > ∣ >= ∣ == ∣ != ‘ `\text{relop}\ \rightarrow \textbf{<=}\ |\ \textbf{<}\ |\ \textbf{>}\ |\ \textbf{>=}\ |\ \textbf{==}\ |\ \textbf{!=}` ‘relop →<= ∣ < ∣ > ∣ >= ∣ == ∣ !=‘
- ‘ additive-expression → additive-expression addop term ∣ term ‘ `\text{additive-expression} \rightarrow \text{additive-expression}\ \text{addop}\ \text{term}\ |\ \text{term}` ‘additive-expression→additive-expression addop term ∣ term‘
- ‘ addop → + ∣ - ‘ `\text{addop} \rightarrow \textbf{+}\ |\ \textbf{-}` ‘addop→+ ∣ -‘
- ‘ term → term mulop factor ∣ factor ‘ `\text{term} \rightarrow \text{term}\ \text{mulop}\ \text{factor}\ |\ \text{factor}` ‘term→term mulop factor ∣ factor‘
- ‘ mulop → * ∣ / ‘ `\text{mulop} \rightarrow \textbf{*}\ |\ \textbf{/}` ‘mulop→* ∣ /‘
- ‘ factor → ( expression ) ∣ var ∣ call ∣ integer ∣ float ‘ `\text{factor} \rightarrow \textbf{(}\ \text{expression}\ \textbf{)}\ |\ \text{var}\ |\ \text{call}\ |\ \text{integer}\ |\ \text{float}` ‘factor→( expression ) ∣ var ∣ call ∣ integer ∣ float‘
- ‘ integer → INTEGER ‘ `\text{integer} \rightarrow \textbf{INTEGER}` ‘integer→INTEGER‘
- ‘ float → FLOATPOINT ‘ `\text{float} \rightarrow \textbf{FLOATPOINT}` ‘float→FLOATPOINT‘
- ‘ call → ID ( args ) ‘ `\text{call} \rightarrow \textbf{ID}\ \textbf{(}\ \text{args} \textbf{)}` ‘call→ID ( args)‘
- ‘ args → arg-list ∣ empty ‘ `\text{args} \rightarrow \text{arg-list}\ |\ \text{empty}` ‘args→arg-list ∣ empty‘
- ‘ arg-list → arg-list , expression ∣ expression ‘ `\text{arg-list} \rightarrow \text{arg-list}\ \textbf{,}\ \text{expression}\ |\ \text{expression}` ‘arg-list→arg-list , expression ∣ expression‘
1.2 Bison 简介
Bison 是一款解析器生成器(parser generator),它可以将 LALR 文法转换成可编译的 C 代码,从而大大减轻程序员手动设计解析器的负担。Bison 是 GNU 对早期 Unix 的 Yacc 工具的一个重新实现,所以文件扩展名为 .y
。(Yacc 的意思是 Yet Another Compiler Compiler。)
每个 Bison 文件由 %%
分成三部分。
%{
#include <stdio.h>
/* 这里是序曲 */
/* 这部分代码会被原样拷贝到生成的 .c 文件的开头 */
int yylex(void);
void yyerror(const char *s);
%}
/* 这些地方可以输入一些 bison 指令 */
/* 比如用 %start 指令指定起始符号,用 %token 定义一个 token */
%start reimu
%token REIMU
%%
/* 从这里开始,下面是解析规则 */
reimu : marisa { /* 这里写与该规则对应的处理代码 */ puts("rule1"); }
| REIMU { /* 这里写与该规则对应的处理代码 */ puts("rule2"); }
; /* 规则最后不要忘了用分号结束哦~ */
/* 这种写法表示 ε —— 空输入 */
marisa : { puts("Hello!"); }
%%
/* 这里是尾声 */
/* 这部分代码会被原样拷贝到生成的 .c 文件的末尾 */
int yylex(void)
{
int c = getchar(); // 从 stdin 获取下一个字符
switch (c) {
case EOF: return YYEOF;
case 'R': return REIMU;
default: return 0; // 返回无效 token 值,迫使 bison 报错
}
}
void yyerror(const char *s)
{
fprintf(stderr, "%s\n", s);
}
int main(void)
{
yyparse(); // 启动解析
return 0;
}
另外有一些值得注意的点:
- Bison 传统上将 token 用大写单词表示,将 symbol 用小写字母表示。
- Bison 能且只能生成解析器源代码(一个
.c
文件),并且入口是yyparse
,所以为了让程序能跑起来,你需要手动提供main
函数(但不一定要在.y
文件中——你懂“链接”是什么,对吧?)。 - Bison 不能检测你的 action code 是否正确——它只能检测文法的部分错误,其他代码都是原样粘贴到
.c
文件中。 - Bison 需要你提供一个
yylex
来获取下一个 token。 - Bison 需要你提供一个
yyerror
来提供合适的报错机制。
顺便提一嘴,上面这个 .y
是可以工作的——尽管它只能接受两个字符串。把上面这段代码保存为 reimu.y
,执行如下命令来构建这个程序:
$ bison reimu.y
$ gcc reimu.tab.c
$ ./a.out
R<-- 不要回车在这里按 Ctrl-D
rule2
$ ./a.out
<-- 不要回车在这里按 Ctrl-D
Hello!
rule1
$ ./a.out
blablabla <-- 回车或者 Ctrl-D
Hello!
rule1 <-- 匹配到了 rule1
syntax error <-- 发现了错误
于是我们验证了上述代码的确识别了该文法定义的语言 { "", "R" }
。
1.3 Bison 和 Flex 的关系
聪明的你应该发现了,我们这里手写了一个 yylex
函数作为词法分析器。而 lab1 我们正好使用 flex 自动生成了一个词法分析器。如何让这两者协同工作呢?特别是,我们需要在这两者之间共享 token 定义和一些数据,难道要手动维护吗?哈哈,当然不用!下面我们用一个四则运算计算器来简单介绍如何让 bison 和 flex 协同工作——重点是如何维护解析器状态、YYSTYPE
和头文件的生成。
首先,我们必须明白,整个工作流程中,bison 是占据主导地位的,而 flex 仅仅是一个辅助工具,仅用来生成 yylex
函数。因此,最好先写 .y
文件。
/* calc.y */
%{
#include <stdio.h>
int yylex(void);
void yyerror(const char *s);
%}
%token RET
%token <num> NUMBER
%token <op> ADDOP MULOP LPAREN RPAREN
%type <num> top line expr term factor
%start top
%union {
char op;
double num;
}
%%
top
: top line {}
| {}
line
: expr RET
{
printf(" = %f\n", $1);
}
expr
: term
{
$$ = $1;
}
| expr ADDOP term
{
switch ($2) {
case '+': $$ = $1 + $3; break;
case '-': $$ = $1 - $3; break;
}
}
term
: factor
{
$$ = $1;
}
| term MULOP factor
{
switch ($2) {
case '*': $$ = $1 * $3; break;
case '/': $$ = $1 / $3; break; // 想想看,这里会出什么问题?
}
}
factor
: LPAREN expr RPAREN
{
$$ = $2;
}
| NUMBER
{
$$ = $1;
}
%%
void yyerror(const char *s)
{
fprintf(stderr, "%s\n", s);
}
/* calc.l */
%option noyywrap
%{
/* 引入 calc.y 定义的 token */
#include "calc.tab.h"
%}
%%
\( { return LPAREN; }
\) { return RPAREN; }
"+"|"-" { yylval.op = yytext[0]; return ADDOP; }
"*"|"/" { yylval.op = yytext[0]; return MULOP; }
[0-9]+|[0-9]+\.[0-9]*|[0-9]*\.[0-9]+ { yylval.num = atof(yytext); return NUMBER; }
" "|\t { }
\r\n|\n|\r { return RET; }
%%
最后,我们补充一个 driver.c
来提供 main
函数。
int yyparse();
int main()
{
yyparse();
return 0;
}
使用如下命令构建并测试程序:
$ bison -d calc.y
(生成 calc.tab.c 和 calc.tab.h。如果不给出 -d 参数,则不会生成 .h 文件。)
$ flex calc.l
(生成 lex.yy.c)
$ gcc lex.yy.c calc.tab.c driver.c -o calc
$ ./calc
1+1
= 2.000000
2*(1+1)
= 4.000000
2*1+1
= 3.000000
如果你复制粘贴了上述程序,可能会觉得很神奇,并且有些地方看不懂。下面就详细讲解上面新出现的各种构造。
-
YYSTYPE
: 在 bison 解析过程中,每个 symbol 最终都对应到一个语义值上。或者说,在 parse tree 上,每个节点都对应一个语义值,这个值的类型是YYSTYPE
。YYSTYPE
的具体内容是由%union
构造指出的。上面的例子中,%union { char op; double num; }
会生成类似这样的代码
typedef union YYSTYPE { char op; double num; } YYSTYPE;
为什么使用
union
呢?因为不同节点可能需要不同类型的语义值。比如,上面的例子中,我们希望ADDOP
的值是char
类型,而NUMBER
应该是double
类型的。 -
$$
和$1
,$2
,$3
, …:现在我们来看如何从已有的值推出当前节点归约后应有的值。以加法为例:term : term ADDOP factor { switch $2 { case '+': $$ = $1 + $3; break; case '-': $$ = $1 - $3; break; } }
其实很好理解。当前节点使用
$$
代表,而已解析的节点则是从左到右依次编号,称作$1
,$2
,$3
… -
%type <>
和%token <>
:注意,我们上面可没有写$1.num
或者$2.op
哦!那么 bison 是怎么知道应该用union
的哪部分值的呢?其秘诀就在文件一开始的%type
和%token
上。例如,
term
应该使用num
部分,那么我们就写%type <num> term
这样,以后用
$
去取某个值的时候,bison 就能自动生成类似stack[i].num
这样的代码了。%token<>
见下一条。 -
%token
:当我们用%token
声明一个 token 时,这个 token 就会导出到.h
中,可以在 C 代码中直接使用(注意 token 名千万不要和别的东西冲突!),供 flex 使用。%token <op> ADDOP
与之类似,但顺便也将ADDOP
传递给%type
,这样一行代码相当于两行代码,岂不是很赚。 -
yylval
:这时候我们可以打开.h
文件,看看里面有什么。除了 token 定义,最末尾还有一个extern YYSTYPE yylval;
。这个变量我们上面已经使用了,通过这个变量,我们就可以在 lexer 里面设置某个 token 的值。
呼……说了这么多,现在回头看看上面的代码,应该可以完全看懂了吧!这时候你可能才意识到为什么 flex 生成的分析器入口是 yylex
,因为这个函数就是 bison 专门让程序员自己填的,作为一种扩展机制。另外,bison(或者说 yacc)生成的变量和函数名通常都带有 yy
前缀,希望在这里说还不太晚……
最后还得提一下,尽管上面所讲已经足够应付很大一部分解析需求了,但是 bison 还有一些高级功能,比如自动处理运算符的优先级和结合性(于是我们就不需要手动把 expr
拆成 factor
, term
了)。这部分功能,就留给同学们自己去探索吧!
添加-个人验证:
2. 实验要求
本次实验需要各位同学首先将自己的 lab1 的词法部分复制到 /src/parser
目录的 lexical_analyzer.l并合理修改相应部分,然后根据 cminus-f
的语法补全 syntax_analyer.y 文件,完成语法分析器,要求最终能够输出解析树。如:
输入:
int bar;
float foo(void) { return 1.0; }
则 parser
将输出如下解析树:
>--+ program
| >--+ declaration-list
| | >--+ declaration-list
| | | >--+ declaration
| | | | >--+ var-declaration
| | | | | >--+ type-specifier
| | | | | | >--* int
| | | | | >--* bar
| | | | | >--* ;
| | >--+ declaration
| | | >--+ fun-declaration
| | | | >--+ type-specifier
| | | | | >--* float
| | | | >--* foo
| | | | >--* (
| | | | >--+ params
| | | | | >--* void
| | | | >--* )
| | | | >--+ compound-stmt
| | | | | >--* {
| | | | | >--+ local-declarations
| | | | | | >--* epsilon
| | | | | >--+ statement-list
| | | | | | >--+ statement-list
| | | | | | | >--* epsilon
| | | | | | >--+ statement
| | | | | | | >--+ return-stmt
| | | | | | | | >--* return
| | | | | | | | >--+ expression
| | | | | | | | | >--+ simple-expression
| | | | | | | | | | >--+ additive-expression
| | | | | | | | | | | >--+ term
| | | | | | | | | | | | >--+ factor
| | | | | | | | | | | | | >--+ float
| | | | | | | | | | | | | | >--* 1.0
| | | | | | | | >--* ;
| | | | | >--* }
请注意,上述解析树含有每个解析规则的所有子成分,包括诸如 ;
{
}
这样的符号,请在编写规则时务必不要忘了它们。
2.1 目录结构
.
├── CMakeLists.txt
├── Documentations
│ ├── lab1
│ └── lab2
│ ├── readings.md <- 扩展阅读
│ └── README.md <- lab2实验文档说明(你在这里)
├── README.md
├── Reports
│ ├── lab1
│ └── lab2
│ └── report.md <- lab2所需提交的实验报告(你需要在此提交实验报告)
├── include <- 实验所需的头文件
│ ├── lexical_analyzer.h
│ └── SyntaxTree.h
├── src <- 源代码
│ ├── common
│ │ └── SyntaxTree.c <- 分析树相关代码
│ ├── lexer
│ └── parser
│ ├── lexical_analyzer.l <- lab1 的词法部分复制到这,并进行一定改写
│ └── syntax_analyzer.y <- lab2 需要完善的文件
└── tests <- 测试文件
├── lab1
└── lab2 <- lab2 测试用例文件夹
2.2 编译、运行和验证
-
编译
与 lab1 相同。若编译成功,则将在
${WORKSPACE}/build/
下生成parser
命令。 -
运行
与
lexer
命令不同,本次实验的parser
命令使用 shell 的输入重定向功能,即程序本身使用标准输入输出(stdin 和 stdout),但在 shell 运行命令时可以使用<
>
和>>
灵活地自定义输出和输入从哪里来。$ cd cminus_compiler-2022-fall $ ./build/parser # 交互式使用(不进行输入重定向) <在这里输入 Cminus-f 代码,如果遇到了错误,将程序将报错并退出。> <输入完成后按 ^D 结束输入,此时程序将输出解析树。> $ ./build/parser < test.cminus # 重定向标准输入 <此时程序从 test.cminus 文件中读取输入,因此不需要输入任何内容。> <如果遇到了错误,将程序将报错并退出;否则,将输出解析树。> $ ./build/parser test.cminus # 不使用重定向,直接从 test.cminus 中读入 $ ./build/parser < test.cminus > out <此时程序从 test.cminus 文件中读取输入,因此不需要输入任何内容。> <如果遇到了错误,将程序将报错并退出;否则,将输出解析树到 out 文件中。>
通过灵活使用重定向,可以比较方便地完成各种各样的需求,请同学们务必掌握这个 shell 功能。
此外,提供了 shell 脚本
/tests/lab2/test_syntax.sh
调用parser
批量分析测试文件。注意,这个脚本假设parser
在项目目录/build
下。# test_syntax.sh 脚本将自动分析 ./tests/lab2/testcase_$1 下所有文件后缀为 .cminus 的文件,并将输出结果保存在 ./tests/lab2/syntree_$1 文件夹下 $ ./tests/lab2/test_syntax.sh easy ... ... ... $ ls ./tests/lab2/syntree_easy <成功分析的文件> $ ./tests/lab2/test_syntax.sh normal $ ls ./tests/lab2/syntree_normal
-
验证
本次试验测试案例较多,为此我们将这些测试分为两类:
- easy: 这部分测试均比较简单且单纯,适合开发时调试。
- normal: 较为综合,适合完成实验后系统测试。
我们使用
diff
命令进行验证。将自己的生成结果和助教提供的xxx_std
进行比较。$ diff ./tests/lab2/syntree_easy ./tests/lab2/syntree_easy_std # 如果结果完全正确,则没有任何输出结果 # 如果有不一致,则会汇报具体哪个文件哪部分不一致 # 使用 -qr 参数可以仅列出文件名
test_syntax.sh
脚本也支持自动调用diff
。# test_syntax.sh 脚本将自动分析 ./tests/lab2/testcase_$1 下所有文件后缀为 .cminus 的文件,并将输出结果保存在 ./tests/lab2/syntree_$1 文件夹下 $ ./tests/lab2/test_syntax.sh easy yes <分析所有 .cminus 文件并将结果与标准对比,仅输出有差异的文件名> $ ./tests/lab2/test_syntax.sh easy verbose <分析所有 .cminus 文件并将结果与标准对比,详细输出所有差异>
请注意助教提供的
testcase
并不能涵盖全部的测试情况,完成此部分仅能拿到基础分,请自行设计自己的testcase
进行测试。
实验要求
本次实验需要各位同学首先将自己的 lab1 的词法部分复制到 /src/parser
目录的 lexical_analyzer.l并合理修改相应部分,然后根据 cminus-f
的语法补全 syntax_analyer.y 文件,完成语法分析器。如:
输入:
int bar;
float foo(void) { return 1.0; }
则 parser
将输出如下解析树:
>--+ program
| >--+ declaration-list
| | >--+ declaration-list
| | | >--+ declaration
| | | | >--+ var-declaration
| | | | | >--+ type-specifier
| | | | | | >--* int
| | | | | >--* bar
| | | | | >--* ;
| | >--+ declaration
| | | >--+ fun-declaration
| | | | >--+ type-specifier
| | | | | >--* float
| | | | >--* foo
| | | | >--* (
| | | | >--+ params
| | | | | >--* void
| | | | >--* )
| | | | >--+ compound-stmt
| | | | | >--* {
| | | | | >--+ local-declarations
| | | | | | >--* epsilon
| | | | | >--+ statement-list
| | | | | | >--+ statement-list
| | | | | | | >--* epsilon
| | | | | | >--+ statement
| | | | | | | >--+ return-stmt
| | | | | | | | >--* return
| | | | | | | | >--+ expression
| | | | | | | | | >--+ simple-expression
| | | | | | | | | | >--+ additive-expression
| | | | | | | | | | | >--+ term
| | | | | | | | | | | | >--+ factor
| | | | | | | | | | | | | >--+ float
| | | | | | | | | | | | | | >--* 1.0
| | | | | | | | >--* ;
| | | | | >--* }
请注意,上述解析树含有每个解析规则的所有子成分,包括诸如 ;
{
}
这样的符号,请在编写规则时务必不要忘了它们。
实验难点
1、如何识别空串产生式
空串产生式的产生,可以通过如下的结构表示:
parent :
| son
| daughter
上述的代码表示 p a r e n t → ε ∣ s o n ∣ d a u g h t e r parent\to \varepsilon\ |\ son\ |\ daughterparent→ε ∣ son ∣ daughter 的语法产生式。
而对于此实验,在语法分析树中,还需添加表示空串的叶结点,用于表示语法单元选择了生成空串的产生式:
parent : { $$ = node("parent", 0);}
| son { $$ = node("parent", 1, $1);}
| daughter { $$ = node("parent", 1, $1);}
2、识别形如 +
的字符
在最开始的时候,我参考了 Cmiuns-f 的语法后,设计的代码中,对于+
等字符的词法单元识别是如下的:
addop : '+'
| '-'
但这样的设计,在添加对应的动作,即{ $$ = node(“addop”, 1, $1);}时,出现了 bug:
尝试了很多解决方法后,要么是not declared的 bug,要么是类型不匹配(string 类型和 node 类型不匹配)的 bug。在仔细学习了 bison 和 flex 的协作模式后,才发现,+等字符实际上是对应 flex 中的词法单元,是由其识别,并返回 ADD等识别结果(token)。而返回的 token 才是 bison 中的语法单元。
因此,将代码修改如下:
addop : ADD { $$ = node("addop", 1, $1); }
| SUB { $$ = node("addop", 1, $1); }
并设置 ADD、SUB 等为 token,类型设置为 node,编译通过,结果正确。
实验设计
1.适当修改lab1的代码,补全至 lexical_analyzer.l文件
观察到pass_node
函数,flex将数值存入yylval,而bison读取yylval之中的值,所以对于每个token,我们都应该创建其的结点。
void pass_node(char *text){
yylval.node = new_syntax_tree_node(text);
}
并且在Example中我们可以看到这样的写法:
/* Example for you :-) */
\+ { pos_start = pos_end; pos_end += 1; pass_node(yytext); return ADD; }
那么,lab1中正则定义即token保持不变,但对于每个token匹配时对应的动作进行修改,都添加一个pass_node(yytext)。因为语法分析树不考虑制表符TAB,注释COMMENT,换行EOL以及空格BLANK,所以不用建立结点不进行返回,只需要把pos_end,pos_start进行修改即可。对于其他未定义的字符只需要printf(“error\n”)。同时lab1中的void analyzer函数也不需要。
代码如下:
/* Example for you :-) */
%%
\+ { pos_start = pos_end; pos_end += 1; pass_node(yytext); return ADD; }
\- { pos_start = pos_end;pos_end++; pass_node(yytext); return SUB;}
\* { pos_start = pos_end;pos_end++; pass_node(yytext); return MUL;}
\/ { pos_start = pos_end;pos_end++; pass_node(yytext); return DIV;}
\< { pos_start = pos_end;pos_end++; pass_node(yytext); return LT;}
\<\= { pos_start = pos_end;pos_end += 2; pass_node(yytext); return LTE;}
\> { pos_start = pos_end;pos_end++; pass_node(yytext); return GT;}
\>\= { pos_start = pos_end;pos_end += 2; pass_node(yytext); return GTE;}
\=\= { pos_start = pos_end;pos_end += 2; pass_node(yytext); return EQ;}
\!\= { pos_start = pos_end;pos_end += 2; pass_node(yytext); return NEQ;}
\= { pos_start = pos_end;pos_end++; pass_node(yytext); return ASSIN;}
\; { pos_start = pos_end;pos_end++; pass_node(yytext); return SEMICOLON;}
\, { pos_start = pos_end;pos_end++; pass_node(yytext); return COMMA;}
\( { pos_start = pos_end;pos_end++; pass_node(yytext); return LPARENTHESE;}
\) { pos_start = pos_end;pos_end++; pass_node(yytext); return RPARENTHESE;}
\[ { pos_start = pos_end;pos_end++; pass_node(yytext); return LBRACKET;}
\] { pos_start = pos_end;pos_end++; pass_node(yytext); return RBRACKET;}
\{ { pos_start = pos_end;pos_end++; pass_node(yytext); return LBRACE;}
\} { pos_start = pos_end;pos_end++; pass_node(yytext); return RBRACE;}
else { pos_start = pos_end;pos_end += 4; pass_node(yytext); return ELSE;}
if { pos_start = pos_end;pos_end += 2; pass_node(yytext); return IF;}
int { pos_start = pos_end;pos_end += 3; pass_node(yytext); return INT;}
float { pos_start = pos_end;pos_end += 5; pass_node(yytext); return FLOAT;}
return { pos_start = pos_end;pos_end += 6; pass_node(yytext); return RETURN;}
void { pos_start = pos_end;pos_end += 4; pass_node(yytext); return VOID;}
while { pos_start = pos_end;pos_end += 5; pass_node(yytext); return WHILE;}
[a-zA-Z]+ { pos_start = pos_end;pos_end += strlen(yytext); pass_node(yytext); return IDENTIFIER;}
[0-9]+ { pos_start = pos_end;pos_end += strlen(yytext); pass_node(yytext); return INTEGER;}
[0-9]+\.|[0-9]*\.[0-9]+ { pos_start = pos_end;pos_end += strlen(yytext); pass_node(yytext); return FLOATPOINT;}
\[\] { pos_start = pos_end;pos_end += 2; pass_node(yytext); return ARRAY;}
[a-zA-Z] { pos_start = pos_end;pos_end++; pass_node(yytext); return LETTER;}
[\n] { pos_start = 1;pos_end = 1;lines++;}
\/\*([*]*(([^*/])+([/])*)*)*\*\/ {}
[ \f\n\r\t\v] { pos_end++;pos_start = pos_end;}
%%
2.根据 cminus-f
的语法补全syntax_analyer.y文件,完成语法分析器
(1)先考虑%union
,因为在 parse tree 上,每个节点都对应一个语义值,这个值的类型是 YYSTYPE。YYSTYPE 的具体内容是由 %union
构造指出的。如图所示,不管是%token
还是%type
,都应该是syntax_tree_node * node
类型,这样才能构建解析树。
/* TODO: Complete this definition. */
%union {syntax_tree_node * node;}
(2)根据lexical_analyzer.l
中的token定义%token<node>
,以及lab2的要求定义%type<node>
。
/* TODO: Your tokens here. */
%token <node> ADD SUB MUL DIV LT LTE GT GTE EQ NEQ ASSIN
SEMICOLON COMMA LPARENTHESE RPARENTHESE LBRACKET RBRACKET
LBRACE RBRACE ELSE IF INT FLOAT RETURN VOID WHILE
IDENTIFIER INTEGER FLOATPOINT ARRAY LETTER
%type <node> program declaration-list declaration var-declaration
type-specifier fun-declaration params param-list param
compound-stmt local-declarations statement-list
statement expression-stmt selection-stmt iteration-stmt
return-stmt expression var simple-expression relop
additive-expression addop term mulop factor integer float call
args arg-list
%start program
(3) 如图所示,根据实验中所给的Cminus-f语法,补充每个%type<node>
的文法解析。
注意的是每个条文法解析后面都要有分号(除了本身给出的第一条),{}里写与该解析对应的处理代码。例如declaration-list
,其第一条解析所得到的是两个解析符号declaration-list declaration
,所以对于该解析要执行的操作是
program : declaration-list
{ $$ = node("program", 1, $1); gt->root = $$; }
再查看 node 函数的函数原型syntax_tree_node *node(const char node_name, int children_num, …),结合实验资料 README.md 中对于 bison 语法的介绍,可以看出 program 为语法分析的起始,该结点类型为 syntax_tree_node。
==KaTeX parse error: Can't use function '$' in math mode at position 23: …("program", 1, $̲1)表示为当前识别到的 pro…,将 program 结点作为根节点,使其作为语法分析树的起始。
因此可以看出,对于语法的分析结构应该为如下的形式:
parent : son { $$ = node("parent", 1, $1);}
| daughter { $$ = node("parent", 1, $1);}
| son AND daughter { $$ = node("parent", 3, $1, $2, $3);}
其中 parent、son、daughter 都是 syntax_tree_node* 类型,都为非终结符;AND 也是 syntax_tree_node* 类型,为终结符。
再结合token和Cminus-f语法:
/* TODO: Your rules here. */
program : declaration-list { $$ = node("program", 1, $1); gt->root = $$; }
declaration-list : declaration-list declaration { $$ = node("declaration-list", 2, $1, $2);}
| declaration { $$ = node("declaration-list", 1, $1); }
declaration : var-declaration { $$ = node("declaration", 1, $1); }
| fun-declaration { $$ = node("declaration", 1, $1); }
var-declaration : type-specifier IDENTIFIER SEMICOLON { $$ = node("var-declaration", 3, $1, $2, $3); }
| type-specifier IDENTIFIER LBRACKET INTEGER RBRACKET SEMICOLON { $$ = node("var-declaration", 6, $1, $2, $3, $4, $5, $6); }
type-specifier : INT { $$ = node("type-specifier", 1, $1); }
| FLOAT { $$ = node("type-specifier", 1, $1); }
| VOID { $$ = node("type-specifier", 1, $1); }
fun-declaration : type-specifier IDENTIFIER LPARENTHESE params RPARENTHESE compound-stmt { $$ = node("fun-declaration", 6, $1, $2, $3, $4, $5, $6); }
params : param-list { $$ = node("params", 1, $1); }
| VOID { $$ = node("params", 1, $1); }
param-list : param-list COMMA param { $$ = node("param-list", 3, $1, $2, $3); }
| param { $$ = node("param-list", 1, $1); }
param : type-specifier IDENTIFIER { $$ = node("param", 2, $1, $2); }
| type-specifier IDENTIFIER ARRAY { $$ = node("param", 3, $1, $2, $3); }
compound-stmt : LBRACE local-declarations statement-list RBRACE { $$ = node("compound-stmt", 4, $1, $2, $3, $4); }
local-declarations : { $$ = node("local-declarations", 0); }
| local-declarations var-declaration { $$ = node("local-declarations", 2, $1, $2); }
statement-list : { $$ = node("statement-list", 0); }
| statement-list statement { $$ = node("statement-list", 2, $1, $2); }
statement : expression-stmt { $$ = node("statement", 1, $1); }
| compound-stmt { $$ = node("statement", 1, $1); }
| selection-stmt { $$ = node("statement", 1, $1); }
| iteration-stmt { $$ = node("statement", 1, $1); }
| return-stmt { $$ = node("statement", 1, $1); }
expression-stmt : expression SEMICOLON { $$ = node("expression-stmt", 2, $1, $2); }
| SEMICOLON { $$ = node("expression-stmt", 1, $1); }
selection-stmt : IF LPARENTHESE expression RPARENTHESE statement { $$ = node("selection-stmt", 5, $1, $2, $3, $4, $5); }
| IF LPARENTHESE expression RPARENTHESE statement ELSE statement { $$ = node("selection-stmt", 7, $1, $2, $3, $4, $5, $6, $7); }
iteration-stmt : WHILE LPARENTHESE expression RPARENTHESE statement { $$ = node("iteration-stmt", 5, $1, $2, $3, $4, $5); }
return-stmt : RETURN SEMICOLON { $$ = node("return-stmt", 2, $1, $2); }
| RETURN expression SEMICOLON { $$ = node("return-stmt", 3, $1, $2, $3); }
expression : var ASSIN expression { $$ = node("expression", 3, $1, $2, $3); }
| simple-expression { $$ = node("expression", 1, $1); }
var : IDENTIFIER { $$ = node("var", 1, $1); }
| IDENTIFIER LBRACKET expression RBRACKET { $$ = node("var", 4, $1, $2, $3, $4); }
simple-expression : additive-expression relop additive-expression { $$ = node("simple-expression", 3, $1, $2, $3); }
| additive-expression { $$ = node("simple-expression", 1, $1); }
relop : LTE { $$ = node("relop", 1, $1); }
| LT { $$ = node("relop", 1, $1); }
| GT { $$ = node("relop", 1, $1); }
| GTE { $$ = node("relop", 1, $1); }
| EQ { $$ = node("relop", 1, $1); }
| NEQ { $$ = node("relop", 1, $1); }
additive-expression : additive-expression addop term { $$ = node("additive-expression", 3, $1, $2, $3); }
| term { $$ = node("additive-expression", 1, $1); }
addop : ADD { $$ = node("addop", 1, $1); }
| SUB { $$ = node("addop", 1, $1); }
term : term mulop factor { $$ = node("term", 3, $1, $2, $3); }
| factor { $$ = node("term", 1, $1); }
mulop : MUL { $$ = node("mulop", 1, $1); }
| DIV { $$ = node("mulop", 1, $1); }
factor : LPARENTHESE expression RPARENTHESE { $$ = node("factor", 3, $1, $2, $3); }
| var { $$ = node("factor", 1, $1); }
| call { $$ = node("factor", 1, $1); }
| integer { $$ = node("factor", 1, $1); }
| float { $$ = node("factor", 1, $1); }
integer : INTEGER { $$ = node("integer", 1, $1); }
float : FLOATPOINT { $$ = node("float", 1, $1); }
call : IDENTIFIER LPARENTHESE args RPARENTHESE { $$ = node("call", 4, $1, $2, $3, $4); }
args : { $$ = node("args", 0); }
| arg-list { $$ = node("args", 1, $1); }
arg-list : arg-list COMMA expression { $$ = node("arg-list", 3, $1, $2, $3); }
| expression { $$ = node("arg-list", 1, $1); }
%%
实验结果验证
编译
运行与验证
1、【实验】交互式使用(不进行输入重定向)
./build/parser
后,可直接输入Cminus-f代码,如果遇到了错误,程序将报错并退出。
输入完成后按^d结束输入。此时程序将输出解析树。
2、运行验证实验是否正确
验证正确
3、自定义验证(符合语法的)
如图所示,设计如下的案例:
执行指令./build/parser < tests/lab2/test0.cminus > tests/lab2/test0_tree
,在没有语法错误的情况下,将解析树定向输出到test1_tree中。(下图是部分截图)
4、自定义验证(不符合语法的)
执行命令./build/parser < tests/lab2/test_wrong.cminus > tests/lab2/test_wrong_tree
因为文法规则中不能在定义变量的时候赋值,所以int b=a;
语句会出现语法错误。如图所示,在第3行第8列即=
出现错误,分析正确。
实验反馈
1、运行的时候,不知道为什么总是找不到文件
后来搜索发现,可能是因为脚本语言不兼容。
通过cat -A filename
语句可以查看脚本文件是什么语言,dos格式的文件行尾为^M$ ,unix格式的文件行尾为$
脚本文件默认是dos格式,我们需要通过 dos2unix filename
来把脚本文件改成Unix格式
2、修改后沿用了实验一的lexical_analyzer.l文件,但是不知道为什么到实验二它就开始报错了……然后这个地方我真的断断续续卡了一个星期都没解决。最后只能是全部推翻重新写过,然后才有了上面的成功结果。
3、在修改 lexical_analyzer.l 的部分时,需要考虑一些特殊的,例如 EOL、COMMENT、BLANK。一开始,对于这些 others,我是直接删去的。但后来在验证的时候,发现如果删去,则无法识别空格、换行符等无关信息,也无法识别注释等内容。这些内容会原封不动地进行输出,而导致输出结果错误(前面空了一大堆,或是存在注释)。随后,我把这些 others 加了回去,且考虑到这些只需要识别,而不需要做语法分析(在语法分析树中不占用内容),故直接去除 return 即可。
参考链接
https://blog.csdn.net/qq_45890970/article/details/123792027
https://blog.csdn.net/m0_51975364/article/details/123237651
https://www.cnblogs.com/yjcs123/p/10862937.html
https://blog.csdn.net/li1325169021/article/details/115361901