编译原理实验三 / 语法分析程序实现

实验要求

实验三 语法分析程序
(一)学习经典的语法分析器
一、实验目的
学习已有编译器的经典语法分析源程序。
二、实验任务
阅读已有编译器的经典语法分析源程序,并测试语法分析器的输出。
三、实验内容
(1)选择一个编译器,如:TINY,其它编译器也可(需自备源代码)。
(2)阅读语法分析源程序,加上你自己的理解。尤其要求对相关函数与重要变量的作用与功能进行稍微详细的描述。若能加上学习心得则更好。TINY语言请参考《编译原理及实践》第3.7节。对TINY语言要特别注意抽象语法树的定义与应用。
(3)测试语法分析器。对TINY语言要求输出测试程序的字符形式的抽象语法树。(手工或编程)画出图形形式的抽象语法树。
TINY语言:
测试用例一:sample.tny。

(二)实现一门语言的语法分析器
一、实验目的
通过本次实验,加深对语法分析的理解,学会编制语法分析器。
二、实验任务
用C或C++语言编写一门语言的语法分析器。
三、实验内容
(1)语言确定:C-语言,其定义在《编译原理及实践》附录A中。也可选择其它语言,不过要有该语言的详细定义(可仿照C-语言)。一旦选定,不能更改,因为要在以后继续实现编译器的其它部分。鼓励自己定义一门语言。也可选择TINY语言,但需要使用与TINY现有语法分析代码不同的分析算法实现,并在实验报告中写清原理。
(2)完成C-语言的BNF文法到EBNF文法的转换。通过这一转换,消除左递归,提取左公因子,将文法改写为LL(1)文法,以适用于自顶向下的语法分析。规划需要将哪些非终结符写成递归下降函数。
(3)为每一个将要写成递归下降函数的非终结符,如:变量声明、函数声明、语句序列、语句、表达式等,定义其抽象语法子树的形式结构,然后定义C-语言的语法树的数据结构。
(4)仿照前面学习的语法分析器,编写选定语言的语法分析器。可以自行选择使用递归下降、LL(0)、LR(0)、SLR、LR(1)中的任意一种方法实现。
(5)准备2~3个测试用例,测试并解释程序的运行结果。

第一部分 学习经典的语法分析器

  • 实验任务:

阅读已有编译器的经典语法分析源程序,并测试语法分析器的输出。
(1)选择一个编译器,如:TINY,其它编译器也可(需自备源代码)。
(2)阅读语法分析源程序,加上你自己的理解。尤其要求对相关函数与重要变量的作用与功能进行稍微详细的描述。若能加上学习心得则更好。TINY语言请参考《编译原理及实践》第3.7节。对TINY语言要特别注意抽象语法树的定义与应用。
(3)测试语法分析器。对TINY语言要求输出测试程序的字符形式的抽象语法树。(手工或编程)画出图形形式的抽象语法树。
TINY语言:
测试用例一:sample.tny。

一. 阅读已有编译器语法分析程序

我选择的经典词法分析程序是TINY源码,下面我就变量作用介绍,函数功能,语法树构建方法以及样例测试这4个方面对TINY词法分析器进行心得总结:

二.变量定义和作用

在头文件globals.h中我们定义了一些节点类型的变量和结构,T I N Y有两种基本的结构类型:语句和表达式。语句共有 5类( i f语句、 r e p e a t语句、 a s s i g n语句、 r e a d语句和w r i t e语句) ,表达式共有 3类(算符表达式、常量表达式和标识符表达式) 。
因此,语法树节点首先按照它是语句还是表达式来分类,接着根据语句或表达式的种类进行再次分类。树节点最大可有 3个孩子的结构(仅在带有 e l s e部分的i f语句中才需要它们) 语句通过同属域而不是使用子域来排序。
①节点类型:

/*用于分析的语法树*/  
typedef enum {StmtK,ExpK} NodeKind;                         //节点类型  

②表达式类型:

typedef enum {IfK,WhileK,ForEach,AssignK,InputK,PrintK} StmtKind;   //语句类(statment)

③表达式种类:

typedef enum {OpK,ConstK,IdK} ExpKind;                      //表达式类(expression)算符表达式、常量表达式和标识符表达式  

④语法树的节点定义:
树的结点种类NodeKind分为StmtK和ExpK两类,两类又有各自的子类。在语法树中标明种类有利于代码生成,当遍历语法树检测到特定类型时就可以进行特定的处理。treeNode结构为指向孩子和兄弟的节点。
语句通过同属域而不是子域来排序,即由父亲到他的孩子的唯一物理连接是到最左孩子的。孩子则在一个标准连接表中自左向右连接到一起,这种连接称作同属连接,用于区别父子连接。

/*ExpType用于类型检查*/  
typedef enum {Void,Integer,Boolean} ExpType;                //表达式种类     
  
#define MAXCHILDREN 3  
  
//语法树的节点  
typedef struct treeNode                   //语法树的节点  
{   
   struct treeNode * child[MAXCHILDREN];  //子节点, 三元  
   struct treeNode * sibling;             //兄弟节点  
   int lineno;                            //当前读取到的行号  
   NodeKind nodekind;                     //节点类型  
   //类型  
   union {                                  
      StmtKind stmt;                      //语句类型  
      ExpKind exp;                        //表达式类型  
   } kind;  
   //属性  
   union {                                  
      TokenType op;                       //操作符  
      int val;                            //值  
      char * name;                        //字段名  
   } attr;  
   //表达式类型  
   ExpType type;                          //表达式类型  
} TreeNode;  

三.函数功能
(1)在文件parse.c中我们生命并且实现了这样的一些函数,这些函数将会帮助构建我们的语法树:
函数 功能

函数功能
static TreeNode * stmt_sequence(void);在循环中匹配分号,再匹配新行(statement)
static void match(TokenType expected)用来判断当前token是否与当前表达式的下一个值匹配
static TreeNode * if_stmt()IF语句规则
static TreeNode * read_stmt(void);循环语句
static TreeNode * write_stmt(void);Write语句
static TreeNode * exp(void);匹配表达式
static TreeNode * simple_exp(void);匹配加减法表达式
static TreeNode * term(void);匹配乘除法表达式
static TreeNode * factor(void);匹配因式项

(2)下面我会对几个关键的函数做详细介绍:

①token是一个静态全局变量这个是主循环。在循环中匹配分号,再匹配新行(statement)

TreeNode * stmt_sequence(void)  
{ TreeNode * t = statement();  
  TreeNode * p = t;  
  while ((token!=ENDFILE) && (token!=END) &&  
         (token!=ELSE) && (token!=UNTIL))  
  { TreeNode * q;  
    match(SEMI);  
    q = statement();  
    if (q!=NULL) {  
      if (t==NULL) t = p = q;  
      else /* now p cannot be NULL either */  
      { p->sibling = q;  
        p = q;  
      }  
    }  
  }  
  return t;  
}  

②然后在statement函数中根据首token的类别通过switch case将解析交给match函数:用来判断当前token是否与当前表达式的下一个值匹配

static void match(TokenType expected)  
{ if (token == expected) token = getToken();  
  else {  
    syntaxError("unexpected token -> ");  
    printToken(token,tokenString);  
    fprintf(listing,"      ");  
  }  
}  

③比如:对if语句来说,if_stmt : IF exp THEN stmt_seq END 在句法的严格限制下,if,then,end三个关键字是必须匹配的,match进行“匹配”确认,错误就报错,成功则运行词法分析读取下一个token

TreeNode * if_stmt(void)  
{ TreeNode * t = newStmtNode(IfK);  
  match(IF);  
  if (t!=NULL) t->child[0] = exp();  
  match(THEN);  
  if (t!=NULL) t->child[1] = stmt_sequence();  
  if (token==ELSE) {  
    match(ELSE);  
    if (t!=NULL) t->child[2] = stmt_sequence();  
  }  
  match(END);  
  return t;  
}  

④总共有五种语句,就不一个个详细分析了。
四.语法树构建方法
(1)用递归下降法写parser程序的过程,就是把图中的文法用程序翻译出来。每个文法都对应一个处理该文法的子例程

比如 factor -> ( exp ) | number 这个文法规则,可以用如下伪代码来表示:  
def factor():  
  switch(token):  
  case ( :   
    match( ( );  
    exp;  
    match( ) );  
  case number:  
    match(number);  
  else error;  
  end switch;   
end  

(2)比较麻烦的是对左递归的处理,递归下降和LL(1)使用不同的方法来处理左递归的问题。递归下降使用拓展巴克斯范式(EBNF)把左递归写成其他形式,类似exp->exp addop term | term这样的表达式是左递归的,并不好直接用递归下降程序来翻译所以通过改写成 exp->term { addop term } 可以方便表示(大括号( { } )内包含的为可重复0至无数次的项。)

exp -> exp + num|num #这里就是递归,我们在定义“exp”这个概念,而它的定义里面又用到了自己本身!
exp -> term { addop term }
#都是匹配形如 num (+ num) (+ num) … 的串 。在数学上,这两个表达式是等价的。

(3)对应的exp匹配程序如下:

def exp():  
  term();    
  while token = '+' or token = '-':  
    match(token);  
    term();  
  end while  
end  

五.样例测试分析
现在需要将语法树结构的描述用图形表示出来,并且画出示例程序的语法树。为了做到这
一点,我们使用矩形框表示语句节点,用圆形框或椭圆形框表示表达式节点。语句或表达式的类型用框中的标记表示,额外的属性在括号中也列出来了。属性指针画在节点框的右边,而子指针则画在框的下面。我们还在图中用三角形表示额外的非指定的树结构,其中用点线表示可能出现也可能不出现的结构。语句序列由同属域连接(潜在的子树由点线和三角形表示) 。则该图如下:

在这里插入图片描述
(1)if 语句(带有3个可能的孩子)如下所示
在这里插入图片描述
(2)repeat 语句有两个孩子。第1个是表示循环体的语句序列,第 2个是一个测试表达式
(3)assign 语句有一个表示其值是被赋予的表达式的孩子(被赋予的变量名保存在语句节点中)
在这里插入图片描述

(4)write 语句也有一个孩子,它表示要写出值的表达式:
(5)算符表达式有两个孩子,它们表示左操作数表达式和右操作数表达式:

在这里插入图片描述

其他所有的节点( r e a d语句、标识符表达式和常量表达式)都是叶子节点。
最后准备显示一个 T I N Y程序的树。程序清单 3 - 3中有来自第1章计算一个整型阶乘的示例程序,它的语法树在图3 - 6中。
在这里插入图片描述

我们将会构建出如图所示的语法树:

在这里插入图片描述

第二部分 实现一门语言的语法分析器

  • 实验内容:

(1)语言确定:
C-语言,其定义在《编译原理及实践》附录A中。也可选择其它语言,不过要有该语言的详细定义(可仿照C-语言)。一旦选定,不能更改,因为要在以后继续实现编译器的其它部分。鼓励自己定义一门语言。也可选择TINY语言,但需要使用与TINY现有语法分析代码不同的分析算法实现,并在实验报告中写清原理。
(2)完成C-语言的BNF文法到EBNF文法的转换:
通过这一转换,消除左递归,提取左公因子,将文法改写为LL(1)文法,以适用于自顶向下的语法分析。规划需要将哪些非终结符写成递归下降函数。
(3)为每一个将要写成递归下降函数的非终结符如:变量声明、函数声明、语句序列、语句、表达式等,定义其抽象语法子树的形式结构,然后定义C-语言的语法树的数据结构。
(4)仿照前面学习的语法分析器,编写选定语言的语法分析器。可以自行选择使用递归下降、LL(0)、LR(0)、SLR、LR(1)中的任意一种方法实现。
(5)准备2~3个测试用例,测试并解释程序的运行结果。

一.语言确定

我在实验3中自定义了一门语言Quary,Quary语言的定义是一个很有挑战性的过程,我模仿C—和python成功定义了它(也许并不完备,随着实验的推进我会一一完善),定义语言的过程中,我对BNF语法有了新的了解和学习。
关于语法和语义,我将仿照C-的定义给出Quary语言的BNF语法:

1.program→declaration-list  
2.declaration-list→declaration-listdeclaration|declaration  
3.declaration→var-declaration|fun-declaration  

程序由声明的列表(或序列)组成,声明可以是函数或变量声明,顺序是任意的。至少必须有一个声明。接下来是语义限制(这些在C中不会出现)。所有的变量和函数在使用前必须声明(这避免了向后backpatching引用)。程序中最后的声明必须是一个函数声明,名字为main。注意,Quary缺乏原型,因此声明和定义之间没有区别

4.var-declaration→type-specifierID;|type-specifierID[NUM];  
5.type-specifier→int|char|void    

变量声明或者声明了简单的整数类型int / 字符类型char变量,或者是基类型为整数/字符的数组变量,索引范围从0到 NUM-1。注意,在一个变量声明中,只能使用类型 指示符int/char。void用于函数声明(参见下面),也要注意,每个声明只能声明一个变量。

6.fun-declaration→type-specifierID(params)compound-stmt  
7.params→param-list|void  
8.param-list→param-list,param|param  
9.param→type-specifierID|type-specifierID[]  

函数声明由返回类型指示符、标识符以及在圆括号内的用逗号分开的参数列表组成,后面
跟着一个复合语句,是函数的代码。如果函数的返回类型是void,那么函数不返回任何值(即
是一个过程)。函数的参数可以是void(即没有参数),或者一列描述函数的参数。参数后面跟
着方括号是数组参数,其大小是可变的。简单的整型参数由值传递。数组参数由引用来传递
(也就是指针),在调用时必须通过数组变量来匹配。注意,类型“函数”没有参数。一个函数
参数的作用域等于函数声明的复合语句,函数的每次请求都有一个独立的参数集。函数可以是递归的(对于使用声明允许的范围)。

10.compound-stmt→:local-declarationsstatement-list end  

复合语句由用: end围起来的一组声明和语句组成。复合语句通过用给定的顺序执行语句
序列来执行。局部声明的作用域等于复合语句的语句列表,并代替任何全局声明。

11.local-declarations→local-declarationsvar-declaration|empty  
12.statement-list→statement-liststatement|empty  

注意声明和语句列表都可以是空的(非终结符empty表示空字符串,有时写作)

13.statement→expression-stmt  
14.|compound-stmt  
15.|selection-stmt  
16.|iteration-stmt  
17.|return-stmt  
18.expression-stmt→expression;|;  

表达式语句有一个可选的且后面跟着分号的表达式。这样的表达式通常求出它们一方的结
果。因此,这个语句用于赋值和函数调用。

19.selection-stmt→if(expression):statement  
20.|if(expression)statement else statement
21.end  

if语句有通常的语义:表达式进行计算;非0值引起第一条语句的执行;0值引起第二条语
句的执行,如果它存在的话。这个规则导致了典型的悬挂else二义性,可以用一种标准的方法解决:else部分通常作为当前if的一个子结构立即分析(“最近嵌套”非二义性规则)。 同样的他们用: end包裹起来。

22.iteration-stmt→while(expression):
23.statement
24.end  

while语句是Quary中的一种重复语句。它重复执行表达式,并且如果表达式的求值为非0,
则执行语句,当表达式的值为0时结束。

25.iteration-stmt→forEach x in list:
26.statement
27.end  

forEach语句是Quary中遍历列表数组的语句,它重复执行表达式,顺序遍历列表直到到达列表尾结束。

28.return-stmt→return;|return expression;  

返回语句可以返回一个值也可无值返回。函数没有说明为void就必须返回一个值。函数
声明为void就没有返回值。return引起控制返回调用者(如果它在main中,则程序结束)。

29.expression→var=expression|simple-expression  
30.var→ID|ID[expression]  

表达式是一个变量引用,后面跟着赋值符号(等号)和一个表达式,或者就是一个简单的表
达式。赋值有通常的存储语义:找到由var表示的变量的地址,然后由赋值符右边的子表达式进行求值,子表达式的值存储到给定的地址。这个值也作为整个表达式的值返回。var是简单的(整型)变量或下标数组变量。负的下标将引起程序停止(与C不同)。然而,不进行下标越界检查。 var表示Quary比C的进一步限制。在C中赋值的目标必须是左值(l-value),左值是可以由许多操作获得的地址。在Quary中唯一的左值是由var语法给定的,因此这个种类按照句法进行检查,代替像C中那样的类型检查。故在Quary中指针运算是禁止的。

31.simple-expression→additive-expressionrelopadditive-expression|additive-expression  
32.relop→<=|<|>|>=|==|!=  

简单表达式由无结合的关系操作符组成(即无括号的表达式仅有一个关系操作符)。简单表
达式在它不包含关系操作符时,其值是加法表达式的值,或者如果关系算式求值为ture,其值为1,求值为false时值为0。

33.additive-expression→additive-expressionaddopterm|term  
34.addop→+|-  
35.term→termmulopfactor|factor  
36.mulop→*|/  

加法表达式和项表示了算术操作符的结合性和优先级。符号表示整数除;即任何余数都被
截去。

37.26.factor→(expression)|var|call|NUM  

因子是围在括号内的表达式;或一个变量,求出其变量的值;或者一个函数调用,求出函
数的返回值;或者一个NUM,其值由扫描器计算。数组变量必须是下标变量,除非表达式由单个ID组成,并且以数组为参数在函数调用中使用(如下所示)。

38.call→ID(args)  
39.args→arg-list|empty  
40.arg-list→arg-list,expression|expression  

函数调用的组成是一个ID(函数名),后面是用括号围起来的参数。参数或者为空,或者由
逗号分割的表达式列表组成,表示在一次调用期间分配的参数的值。函数在调用之前必须声明,声明中参数的数目必须等于调用中参数的数目。函数声明中的数组参数必须和一个表达式匹配,这个表达式由一个标识符组成表示一个数组变量。最后,上面的规则没有给出输入和输出语句。在C-的定义中必须包含这样的函数,因为 与C不同,C-没有独立的编译和链接工具;因此,考虑两个在全局环境中预定义的函数,好像它们已进行了声明:

41.Int input(void){...}  
42.Void print(int x | int x[] |char x | char x[]){...}  

input函数没有参数,从标准输入设备(通常是键盘)返回一个整数值。print函数接受
一个整型参数或整型数组或字符参数或字符数组,其值和一个换行符一起打印到标准输出设备(通常是屏幕)。

二.完成BNF到EBNF的转换

递归下降使用拓展巴克斯范式(EBNF)把左递归写成其他形式,类似exp->exp addop term | term这样的表达式是左递归的,并不好直接用递归下降程序来翻译所以通过改写成 exp->term { addop term } 可以方便表示(大括号( { } )内包含的为可重复0至无数次的项。)

exp -> exp + num|num   #这里就是递归,我们在定义“exp”这个概念,而它的定义里面又用到自己本身! 
exp -> term { addop term }  
#都是匹配形如  num (+ num) (+ num) ...  的串 。在数学上,这两个表达式是等价的。  

对应的exp匹配程序如下:

def exp():
  term();  
  while token = '+' or token = '-':
    match(token);
    term();
  end while
end

我们只需要将文法定义中的左递归BNF按照规则exp->exp addop term | term改写成 exp->term { addop term } 进行替换即可。

三.定义抽象语法树形式结构

对于抽象语法树的结构,我仿照TINY的语法分析器来定义它。在头文件globals.h中我们定义了一些节点类型的变量和结构,Quary

1.变量和结构
(1)有两种基本的结构类型:语句StmtK和表达式ExpK。
(2)语句共有 5类( i f语句、 while语句、 assign语句、 Input语句和PrintK语句)
(3)表达式共有 3类(算符表达式OpK、常量表达式ConstK和标识符表达式IdK) 。

因此,语法树节点首先按照它是语句还是表达式来分类,接着根据语句或表达式的种类进行再次分类。树节点最大可有 3个孩子的结构(仅在带有 e l s e部分的i f语句中才需要它们) 语句通过同属域而不是使用子域来排序。
节点类型:

/*用于分析的语法树*/  
typedef enum {StmtK,ExpK} NodeKind;                         //节点类型  

表达式类型:

typedef enum {IfK,WhileK,ForEach,AssignK,InputK,PrintK} StmtKind;   //语句类(statment) 

表达式种类:

typedef enum {OpK,ConstK,IdK} ExpKind;                      //表达式类(expression)算符表达式、常量表达式和标识符表达式  

语法树的节点定义:
树的结点种类NodeKind分为StmtK和ExpK两类,两类又有各自的子类。在语法树中标明种类有利于代码生成,当遍历语法树检测到特定类型时就可以进行特定的处理。treeNode结构为指向孩子和兄弟的节点。
语句通过同属域而不是子域来排序,即由父亲到他的孩子的唯一物理连接是到最左孩子的。孩子则在一个标准连接表中自左向右连接到一起,这种连接称作同属连接,用于区别父子连接。

/*ExpType用于类型检查*/  
typedef enum {Void,Integer,Boolean} ExpType;                //表达式种类     
  
#define MAXCHILDREN 3  
  
//语法树的节点  
typedef struct treeNode                   //语法树的节点  
{   
   struct treeNode * child[MAXCHILDREN];  //子节点, 三元  
   struct treeNode * sibling;             //兄弟节点  
   int lineno;                            //当前读取到的行号  
   NodeKind nodekind;                     //节点类型  
   //类型  
   union {                                  
      StmtKind stmt;                      //语句类型  
      ExpKind exp;                        //表达式类型  
   } kind;  
   //属性  
   union {                                  
      TokenType op;                       //操作符  
      int val;                            //值  
      char * name;                        //字段名  
   } attr;  
   //表达式类型  
   ExpType type;                          //表达式类型  
} TreeNode;  

2.函数功能
(1)在文件parse.c中我们生命并且实现了这样的一些函数,这些函数将会帮助构建我们的语法树:
函数 功能

函数功能
static TreeNode * stmt_sequence(void);在循环中匹配分号,再匹配新行(statement)
static void match(TokenType expected)用来判断当前token是否与当前表达式的下一个值匹配
static TreeNode * if_stmt()IF语句规则
static TreeNode * while_stmt(void);循环语句
static TreeNode * input_stmt(void);Write语句
static TreeNode * simple_exp(void);匹配加减法表达式
static TreeNode * term(void);匹配乘除法表达式
static TreeNode * factor(void);匹配因式项
static TreeNode * forEach_stmt(void);循环语句规则: forEach
static TreeNode * assign_stmt(void);赋值语句规则: assign
static TreeNode * print_stmt(void);输出语法规则PRINT
static TreeNode * express(void);逻辑表达式
static TreeNode * compareExpress(void);大小比较表达式

四.仿照语法分析器,编写选定语言的语法分析器

(1)实现思路:
在编写语法分析器时,我选用的方法是递归下降法,基本思路是把图中的文法用程序翻译出来。每个文法都对应一个处理该文法的子例程。每次用stmt_sequence在循环中匹配新的行,然后用match匹配语句开头的第一个字段,判断不同的文法,交给不同的文法处理程序处理。其处理程序的结构关系图如下:
在这里插入图片描述

经过statement匹配的语句,交给不同的文法处理器处理,包括了if_stmt, while_stmt, forEach_stmt, assign_stmt, input_stmt, print_stmt, 如果匹配错误将会触发异常。在每个语法处理程序中,包含了表达式节点,这个时候语法处理程序将会有express匹配表达式,表达式采用递归的形式构建,按照逻辑顺序,会先后由逻辑表达式节点,大小判断节点,加减法节点,乘除法节点,项节点处理,然后每一个项又可能有新的express组成,这样循环递归执行,直到表达式匹配到表达式类型(expression)算符表达式、常量表达式和标识符表达式{OpK,ConstK,IdK}终止。

(2)下面我会对几个关键的函数做详细介绍:
①stmt_sequence函数在循环中匹配分号,再匹配新的行

//stmt_sequence->stmt_sequence;statement | statement  
TreeNode * stmt_sequence(void)        //在循环中匹配分号,再匹配新行(statement)  
{  
  TreeNode * t = statement();  
  TreeNode * p = t;  
  // if(token==SEMI) match(SEMI);  
  // if(token==ENDFILE) match(ENDFILE);  
  // if(token==SEMI) match(SEMI);  
  //当遇到文件结束, END保留字的时候单独一行  
  while ((token!=ENDFILE) && (token!=END) &&(token!=ELSE))  
  {  
    TreeNode * q;  
    //匹配当前是不是; 或 :号, 这个时候表示新的一行结束  
    // if(token==SEMI)   
    match(SEMI);  
    // else  
    //   match(COLON);  
    q = statement();  //增加兄弟节点    
    if (q!=NULL) {  
      if (t==NULL) t = p = q;  
      else /* now p cannot be NULL either */  
      {   
        p->sibling = q;  
        p = q;  
      }  
    }  
  }  
  return t;  
}  

② token是一个静态全局变量这个是主循环。在循环中匹配分号,再匹配新行(statement)

TreeNode * stmt_sequence(void)  
{ TreeNode * t = statement();  
  TreeNode * p = t;  
  while ((token!=ENDFILE) && (token!=END) &&  
         (token!=ELSE) && (token!=UNTIL))  
  { TreeNode * q;  
    match(SEMI);  
    q = statement();  
    if (q!=NULL) {  
      if (t==NULL) t = p = q;  
      else /* now p cannot be NULL either */  
      { p->sibling = q;  
        p = q;  
      }  
    }  
  }  
  return t;  
}  

③ 然后在statement函数中根据首token的类别通过switch case将解析交给match函数:用来判断当前token是否与当前表达式的下一个值匹配

/* 
statement → expression-stmt |  statement 
*/  
//产生新的语句类型,判断当前开头的字符,决定不同的语法  
TreeNode * statement(void)  
{   
  TreeNode * t = NULL;  
  switch (token) {  
    case IF : t = if_stmt(); break;                   //IF  
    case WHILE : t = while_stmt(); break;             //WHILE  
    case FOREACH: t=forEach_stmt(); break;            //FOREACH  
    case ID : t = assign_stmt(); break;               //赋值  
    case INPUT: t = input_stmt(); break;              //输入    
    case PRINT: t = print_stmt(); break;              //输出  
    // case SEMI: match(SEMI);break;  
    default :   
      syntaxError("unexpected token -> ");            //其他状态认为是错误状态  
      printToken(token,tokenString);  
      token = getToken();  
      break;  
  }  
  return t;  
}  

④ 对if语句来说,if_stmt : IF exp THEN stmt_seq END 在句法的严格限制下,if,then,end三个关键字是必须匹配的,match进行“匹配”确认,错误就报错,成功则运行词法分析读取下一个token

TreeNode * if_stmt(void)  
{ TreeNode * t = newStmtNode(IfK);  
  match(IF);  
  if (t!=NULL) t->child[0] = exp();  
  match(THEN);  
  if (t!=NULL) t->child[1] = stmt_sequence();  
  if (token==ELSE) {  
    match(ELSE);  
    if (t!=NULL) t->child[2] = stmt_sequence();  
  }  
  match(END);  
  return t;  
}  

⑤对于WHILE循环语句,他总是先匹配保留字WHILE,然后再匹配接着他的”:”, 然后匹配执行语句,他只有一个子节点,递归使用stmt_sequenc(), 最后匹配终结符号end。

//循环语句规则: WHILE  
TreeNode * while_stmt(void)  
{   
  TreeNode * t = newStmtNode(WhileK);                   //产生一个新的语句WHILE点  
  match(WHILE);                                         //匹配WHILE  
  if (t!=NULL) t->child[0] = express();                 //匹配紧跟着的条件表达式  
  match(COLON);                                         //匹配:  
  if (t!=NULL) t->child[1] = stmt_sequence();           //匹配执行语句  
  match(END);                                           //匹配一个END  
  return t;  
}  

⑥对于赋值语句语句,他总是先设置属性name为当前的串,然后匹配一个新的ID和=号,最后通过express匹配子节点表达式。

//赋值语句规则: assign    
TreeNode * assign_stmt(void)    
{     
  TreeNode * t = newStmtNode(AssignK);    
  if ((t!=NULL) && (token==ID))                         //name属性    
    t->attr.name = copyString(tokenString);    
  match(ID);                                            //匹配一个ID    
  match(ASSIGN);                                        //匹配一个=    
  if (t!=NULL) t->child[0] = express();                     //匹配表达式    
  return t;    
}    

⑦ 输入输出语法类似,首先匹配保留字,然后添加name属性,对于PRINT,子节点点则是一个表达式。

//输入语法规则INPUT  
TreeNode * input_stmt(void)  
{   
  TreeNode * t = newStmtNode(InputK);  
  match(INPUT);                                         //匹配INPUT保留字  
  if ((t!=NULL) && (token==ID))             
    t->attr.name = copyString(tokenString);             //添加属性      
  match(ID);  
  return t;  
}  
  
//输出语法规则PRINT  
TreeNode * print_stmt(void)  
{   
  TreeNode * t = newStmtNode(PrintK);             
  match(PRINT);                                         //匹配输出PRINT  
  if (t!=NULL) t->child[0] = express();                 //添加子节点为表达式  
  return t;  
}  

⑦下面是表达式的处理,表达式处理具有高度的一致性,表达式采用递归的形式构建,按照逻辑顺序,会先后由逻辑表达式节点,大小判断节点,加减法节点,乘除法节点,项节点处理,然后每一个项又可能有新的express组成,这样循环递归执行,直到表达式匹配到表达式类型(expression)算符表达式、常量表达式和标识符表达式{OpK,ConstK,IdK}终止。如下图:
在这里插入图片描述

//大小比较表达式  
//var = compareExpress | simple_exp  
TreeNode * compareExpress(void)  
{   
  TreeNode * t = simple_exp();  
  if ((token==LS)||(token==LE)||(token==GT)||(token==GE)||(token==EQ)||(token==NE)){  
    TreeNode * p = newExpNode(OpK);                     //新建一个表达式节点, 类型为算符表达式  
    if (p!=NULL) {  
      p->child[0] = t;                                  //  
      p->attr.op = token;                               //操作符为token  
      t = p;                                            //  
    }  
    match(token);                                       //匹配当前字符防止检测发生  
    if (t!=NULL)                                  
      t->child[1] = simple_exp();                       //子表达式  
  }  
  return t;  
}  

上面是一大小表达式的例子,剩下的大小表达式compareExpress,加减法表达式simple_exp,乘除法表达式term与此类似。

⑧项的处理:
一个表达式expresss可能由子表达式, ID或NUM组成,也就是ENBF表达式(expression) | NUM | ID ,因此我们可以这样处理项,判断当前的串如果是一个NUM,那么新建一个子节点,将它的val设置为当前串的值; 如果当前串是ID,那么新建节点设置属性为当前串; 如果当前串是( 那么要匹配子表达式调用函数express,然后匹配)号。

TreeNode * factor(void)                               
{   
  TreeNode * t = NULL;  
  switch (token) {  
    case NUM :                            //NUM类型表达式  
      t = newExpNode(ConstK);  
      if ((t!=NULL) && (token==NUM))  
        t->attr.val = atoi(tokenString);  
      match(NUM);  
      break;  
    case ID :                             //ID标识符类型表达式  
      t = newExpNode(IdK);  
      if ((t!=NULL) && (token==ID))  
        t->attr.name = copyString(tokenString);  
      match(ID);  
      break;  
    case LPAREN :                         //左括号表达式     
      match(LPAREN);  
      t = express();  
      match(RPAREN);                      //匹配右括号  
      break;  
    // case SEMI: match(SEMI);break;  
    default:  
      syntaxError("unexpected token -> ");  
      printToken(token,tokenString);  
      token = getToken();  
      break;  
    }  
  return t;  
}  

经过上面的处理,语法树就能成功构建起来,为了输出语法树,我们还需要借助工具类中的函数printTree将节点打印出来,基本思路很简单,对于当前的一个节点,我们要先判断他的类型,如果是语句类型,直接打印;如果是表达式类型,那么打印属性。然后扩展所有的子节点。

void printTree( TreeNode * tree )  
{ int i;  
  INDENT;  
  while (tree != NULL) {  
    printSpaces();  
    if (tree->nodekind==StmtK)            //如果是语句类型  
    {   
      switch (tree->kind.stmt) {  
        case IfK,WhileK,ForEach,AssignK,InputK,PrintK:  
      }  
    }  
    else if (tree->nodekind==ExpK)        //如果是表达式类型  
    {   
      switch (tree->kind.exp) {  
        case OpK:  
          printToken(tree->attr.op,"\0");  
        case ConstK:  
          fprintf(listing,"Const: %d\n",tree->attr.val);  
        case IdK:  
          fprintf(listing,"Id: %s\n",tree->attr.name);  
      }  
    }  
    //扩展兄弟节点  
    for (i=0;i<MAXCHILDREN;i++)       //兄弟节点  
         printTree(tree->child[i]);  
    tree = tree->sibling;  
  }  
  UNINDENT;  
}  

五.样例测试

我共设置了三组样例,下面我将会对他们一一分析:
(1)样例1:
下图左侧样例1是一个简单的if条件语句测试,首先使用input输入一个值x,然后用对x进行判断,如果x小于0,那么facts=0;如果x> 2, 那么facts=1, 如果x > 3, 那么facts=2, 否则facts=3。最后将facts用print打印出来。

执行命令:$ ./QuaryParse samples/sample1.py

在这里插入图片描述

上图右侧是样例1的执行结果,首先语法树的第一层应该有两个节点,也就是input和IF,然后IF语句内部则有两个分支,一个是从fact=0到end,另外一个是else部分,在地一个分支内部存在一个IF语句,同样的,IF语句内部又有一个IF语句嵌套,因此有三层IF,然后处理Else分支,在Else分支中只有两个语句,一个是赋值语句fact=3,相应的语法树结果为Assign to: fact,Const: 3 最后是Print语句,内部是一个表达式ID :fact,观察测试结果,符合预期。

(2)样例2:
下图左侧样例2是一个while-if-else分支测试,程序求和0~10的平方和,输入一个值,判断sum和他的大小关系,下面是测试结果:
在这里插入图片描述

观察测试代码,语法树第一层有4个语句节点,分别是Input,Assign,Assign,While,If,然后我们看到重点位置while循环,在while循环内部有3条语句,因此有三个语句节点,他们分别是赋值语句Assign,Assign,打印语句Print,在while语句的后面,我们还有一个表达式节点,他用于确定循环执行的条件,Op: <= \ Id: i \ Const: 10,最后IF语句有两个分支,后面紧跟一个表达式节点Op: > \ Id: sum \ Id: y ,接下来就是else分支,Print \ Const: 1,观察测试结果,符合分析预期。

(3)样例3:
sample3.qy 测试样3, 它计算一个斐波拉契数列Fibonacci的值,输入一个数x,程序计算Fibonacci(x) 并且打印,下面我将仔细分析语法树的结果。
在这里插入图片描述

观察测试代码,语法树第一层有2个语句节点,分别是Input,If。第一个节点Input: n输入n的值,然后来到IF节点,他只是简单判断n<0 , 关键部分还在ELSE分支,在分支中,有一个逻辑表达式,if((n0) or (n1)):,程序将条件识别为两个表达式,Op: == \ Id: n \ Const: 0 然后两个express用保留子OR组合成一个新的表达式节点。在IF的子分支ELSE中,是一连串的数据赋值操作,毫无疑问他们被处理为Assign to: \ const,最后是一段while循环,首先生成表达式节点express,然后是一段连续的赋值交换操作和表达式计算 Assign to: res \ Op: + Id: a \ Id: b, 观察测试结果,符合分析预期。

这个里面的都是测试数据,总共得分5分。从控制台输入,不能从文件中读取。实现了基本功能,加分项目都没有去实现,没有函数数组这些的实现。这是用C++语言写的,新建parser类别要选C++,其他对于VS的配置和C语言一样。for语句用的是枚举所有情况,你可以自行修改。 对预备工作中自然语言描述的简化C编译器的语言特性的语法,设计上下文无关文法进行描述 借助Yacc工具实现语法分析器 考虑语法树的构造: 1.语法树数据结构的设计:节点类型的设定,不同类型节点应保存哪些信息,多叉树的实现方式 2.实现辅助函数,完成节点创建、树创建等功能 3.利用辅助函数,修改上下文无关文法,设计翻译模式 4.修改Yacc程序,实现能构造语法树的分析器 考虑符号表处理的扩充 1.完成语法分析后,符号表项应增加哪些标识符的属性,保存语法分析的结果 2.如何扩充符号表数据结构,Yacc程序如何与Lex程序交互,正确填写符号表项 以一个简单的C源程序验证你的语法分析器,可以文本方式输出语法树结构,以节点编号输出父子关系,来验证分析器的正确性,如下例: main() { int a, b; if (a == 0) a = b + 1; } 可能的输出为: 0 : Type Specifier, integer, Children: 1 : ID Declaration, symbol: a Children: 2 : ID Declaration, symbol: b Children: 3 : Var Declaration, Children: 0 1 2 4 : ID Declaration, symbol: a Children: 5 : Const Declaration, value:0, Children: 6 : Expr, op: ==, Children: 4 5 7 : ID Declaration, symbol: a Children: 8 : ID Declaration, symbol: b Children: 9 : Const Declaration, value:1, Children: 10: Expr, op: +, Children: 8 9 11: Expr, op: =, Children: 7 10 12: if statement, Children: 6 11 13: compound statement, Children: 3 12
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值