编译原理lab2-cminus_compiler-2022-fall

LAB2实验报告文档

  1. 了解 bison 基础知识和理解 Cminus-f 语法(重在了解如何将文法产生式转换为 bison 语句)
  2. 阅读 /src/common/SyntaxTree.c,对应头文件 /include/SyntaxTree.h(重在理解分析树如何生成)
  3. 了解 bisonflex 之间是如何协同工作,看懂pass_node函数并改写 Lab1 代码(提示:了解 yylval 是如何工作,在代码层面上如何将值传给$1$2等)
  4. 补全 src/parser/syntax_analyzer.y 文件和 lexical_analyzer.l 文件

Tips:在未编译的代码文件中是无法看到关于协同工作部分的代码,建议先编译 1.3 给出的计算器样例代码,再阅读 /build/src/parser/ 中的 syntax_analyzer.hsyntax_analyzer.c 文件

思考题

本部分不算做实验分,出题的本意在于想要帮助同学们加深对实验细节的理解,欢迎有兴趣和余力的同学在报告中写下你的思考答案,或者在issue中分享出你的看法。

  1. 在1.3样例代码中存在左递归文法,为什么 bison 可以处理?(提示:不用研究bison内部运作机制,在下面知识介绍中有提到 bison 的一种属性,请结合课内知识思考)

  2. 请在代码层面上简述下 yylval 是怎么完成协同工作的。(提示:无需研究原理,只分析维护了什么数据结构,该数据结构是怎么和$1$2等联系起来?)

  3. 请尝试使用1.3样例代码运行除法运算除数为0的例子(测试case中有)看下是否可以通过,如果不,为什么我们在case中把该例子认为是合法的?(请从语法与语义上简单思考)

  4. 能否尝试修改下1.3计算器文法,使得它支持除数0规避功能。

1.基础知识

我们在这里简单介绍如何让 bisonflex 协同工作及其原理,并简单介绍 bison 的一些基础知识。

1.1 Cminus-f 语法

本小节将给出Cminus-f的语法,该语法在Cminus语言的基础上增加了float类型。

Cminus的详情请参考《编译原理与实践》第九章附录。

我们将 Cminus-f 的所有规则分为五类。

  1. 字面量、关键字、运算符与标识符
    • id
    • type-specifier
    • relop
    • addop
    • mulop
  2. 声明
    • declaration-list
    • declaration
    • var-declaration
    • fun-declaration
    • local-declarations
  3. 语句
    • compound-stmt
    • statement-list
    • statement
    • expression-stmt
    • iteration-stmt
    • selection-stmt
    • return-stmt
  4. 表达式
    • expression
    • var
    • additive-expression
    • term
    • factor
    • integer
    • float
    • call
  5. 其他
    • params
    • param-list
    • param
    • args
    • arg-list

起始符号是 program

Cminus-f语法

  1. ‘ program → declaration-list ‘ `\text{program} \rightarrow \text{declaration-list}` programdeclaration-list
  2. ‘ declaration-list → declaration-list declaration  ∣  declaration ‘ `\text{declaration-list} \rightarrow \text{declaration-list}\ \text{declaration}\ |\ \text{declaration}` declaration-listdeclaration-list declaration  declaration
  3. ‘ declaration → var-declaration  ∣  fun-declaration ‘ `\text{declaration} \rightarrow \text{var-declaration}\ |\ \text{fun-declaration}` declarationvar-declaration  fun-declaration
  4. ‘ 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 ] ;
  5. ‘ type-specifier → int   ∣   float   ∣   void ‘ `\text{type-specifier} \rightarrow \textbf{int}\ |\ \textbf{float}\ |\ \textbf{void}` type-specifierint  float  void
  6. ‘ 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-declarationtype-specifier ID ( params ) compound-stmt
  7. ‘ params → param-list  ∣   void ‘ `\text{params} \rightarrow \text{param-list}\ |\ \textbf{void}` paramsparam-list  void
  8. ‘ param-list → param-list  ,  param  ∣  param ‘ `\text{param-list} \rightarrow \text{param-list}\ ,\ \text{param}\ |\ \text{param}` param-listparam-list , param  param
  9. ‘ param → type-specifier  ID   ∣  type-specifier  ID   [] ‘ `\text{param} \rightarrow \text{type-specifier}\ \textbf{ID}\ |\ \text{type-specifier}\ \textbf{ID}\ \textbf{[]}` paramtype-specifier ID  type-specifier ID []
  10. ‘ compound-stmt → {  local-declarations statement-list } ‘ `\text{compound-stmt} \rightarrow \textbf{\{}\ \text{local-declarations}\ \text{statement-list} \textbf{\}}` compound-stmt{ local-declarations statement-list}
  11. ‘ local-declarations → local-declarations var-declaration  ∣  empty ‘ `\text{local-declarations} \rightarrow \text{local-declarations var-declaration}\ |\ \text{empty}` local-declarationslocal-declarations var-declaration  empty
  12. ‘ statement-list → statement-list statement  ∣  empty ‘ `\text{statement-list} \rightarrow \text{statement-list}\ \text{statement}\ |\ \text{empty}` statement-liststatement-list statement  empty
  13. ‘ 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
  14. ‘ expression-stmt → expression  ;   ∣   ; ‘ `\text{expression-stmt} \rightarrow \text{expression}\ \textbf{;}\ |\ \textbf{;}` expression-stmtexpression ;  ;
  15. ‘ 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
  16. ‘ iteration-stmt → while   (  expression  )  statement ‘ `\text{iteration-stmt} \rightarrow \textbf{while}\ \textbf{(}\ \text{expression}\ \textbf{)}\ \text{statement}` iteration-stmtwhile ( expression ) statement
  17. ‘ return-stmt → return   ;   ∣   return  expression  ; ‘ `\text{return-stmt} \rightarrow \textbf{return}\ \textbf{;}\ |\ \textbf{return}\ \text{expression}\ \textbf{;}` return-stmtreturn ;  return expression ;
  18. ‘ expression → var  =  expression  ∣  simple-expression ‘ `\text{expression} \rightarrow \text{var}\ \textbf{=}\ \text{expression}\ |\ \text{simple-expression}` expressionvar = expression  simple-expression
  19. ‘ var → ID   ∣   ID   [  expression ] ‘ `\text{var} \rightarrow \textbf{ID}\ |\ \textbf{ID}\ \textbf{[}\ \text{expression} \textbf{]}` varID  ID [ expression]
  20. ‘ 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-expressionadditive-expression relop additive-expression  additive-expression
  21. ‘ relop  → <=   ∣   <   ∣   >   ∣   >=   ∣   ==   ∣   != ‘ `\text{relop}\ \rightarrow \textbf{<=}\ |\ \textbf{<}\ |\ \textbf{>}\ |\ \textbf{>=}\ |\ \textbf{==}\ |\ \textbf{!=}` relop <=  <  >  >=  ==  !=
  22. ‘ additive-expression → additive-expression addop term  ∣  term ‘ `\text{additive-expression} \rightarrow \text{additive-expression}\ \text{addop}\ \text{term}\ |\ \text{term}` additive-expressionadditive-expression addop term  term
  23. ‘ addop → +   ∣   - ‘ `\text{addop} \rightarrow \textbf{+}\ |\ \textbf{-}` addop+  -
  24. ‘ term → term mulop factor  ∣  factor ‘ `\text{term} \rightarrow \text{term}\ \text{mulop}\ \text{factor}\ |\ \text{factor}` termterm mulop factor  factor
  25. ‘ mulop → *   ∣   / ‘ `\text{mulop} \rightarrow \textbf{*}\ |\ \textbf{/}` mulop*  /
  26. ‘ 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
  27. ‘ integer → INTEGER ‘ `\text{integer} \rightarrow \textbf{INTEGER}` integerINTEGER
  28. ‘ float → FLOATPOINT ‘ `\text{float} \rightarrow \textbf{FLOATPOINT}` floatFLOATPOINT
  29. ‘ call → ID   (  args ) ‘ `\text{call} \rightarrow \textbf{ID}\ \textbf{(}\ \text{args} \textbf{)}` callID ( args)
  30. ‘ args → arg-list  ∣  empty ‘ `\text{args} \rightarrow \text{arg-list}\ |\ \text{empty}` argsarg-list  empty
  31. ‘ arg-list → arg-list  ,  expression  ∣  expression ‘ `\text{arg-list} \rightarrow \text{arg-list}\ \textbf{,}\ \text{expression}\ |\ \text{expression}` arg-listarg-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;
}

另外有一些值得注意的点:

  1. Bison 传统上将 token 用大写单词表示,将 symbol 用小写字母表示。
  2. Bison 能且只能生成解析器源代码(一个 .c 文件),并且入口是 yyparse,所以为了让程序能跑起来,你需要手动提供 main 函数(但不一定要在 .y 文件中——你懂“链接”是什么,对吧?)。
  3. Bison 不能检测你的 action code 是否正确——它只能检测文法的部分错误,其他代码都是原样粘贴到 .c 文件中。
  4. Bison 需要你提供一个 yylex 来获取下一个 token。
  5. 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 上,每个节点都对应一个语义值,这个值的类型是 YYSTYPEYYSTYPE 的具体内容是由 %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
    
  • 验证

    本次试验测试案例较多,为此我们将这些测试分为两类:

    1. easy: 这部分测试均比较简单且单纯,适合开发时调试。
    2. 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

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值