实验要求
实验三 语法分析程序
(一)学习经典的语法分析器
一、实验目的
学习已有编译器的经典语法分析源程序。
二、实验任务
阅读已有编译器的经典语法分析源程序,并测试语法分析器的输出。
三、实验内容
(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, 观察测试结果,符合分析预期。