转自:https://blog.csdn.net/wyt734933289/article/details/53956151
预备知识
编译器的前端是由三个模块和两个核心数据结构(记号流,抽象语法树)组成的,
编译器处于一个流水线的结构,阶段无关性(只考虑把每一个模块的输入输出)
语义分析只依赖于前一阶段的抽象语法树
语义分析也称为类型检查。上下文相关分析。负责检查程序(抽象语法树)的上下文相关的属性:
变量在使用前先声明
每个表达式都有何时的类型
函数调用和函数的定义一致
………………
举个例子
void f(int *p)
{
X += 4:
Cal(X);
break;
}
1
2
3
4
5
6
编译器报出x,cal没有定义的错误,没有找到循环的错误,这就是所谓的上下文相关,不仅仅是检查当前
所以语义分析要完成的工作就是针对给定的一段代码,去找出所有的语义错误,如果出错了,要给出清晰的诊断信息反馈给程序员,程序员根据出错信息改错
语义分析器接受一个抽象语法树的输入,这棵树必然满足语法正确,还有一个输入,就是程序语言的合法规则(表明什么是合法的,什么是非法的),输出Yes或者NO,如果是Yes,那么就要产生中间代码(这个阶段过后,程序就不应包含任何语法和语义错误),如果是NO,就要给出错误信息
如何定义程序语言的语义?
传统上,大部分的程序设计语言都采用自然语言来表达程序语言的语义
例如,对于“+”运算
要求左右操作数都必须是整形数,如e1 + e2, 对于语法分析,+号两边操作数是不管的,而语义分析就要检查是否为整形数
这个自然语言是我们学习一门语言的所需要的,比如一本编程书上
但编译器的实现者必须对语言中的语义规定有全面的理解,比如加号两边可以是浮点数和整形数
那么如何能够正确高效地实现?
给出下面文法举个例子
定义加法左右两边只能是整数
E->N
| true
| false
| E + E
| E && E
1
2
3
4
5
类型合法的程序
3 + 4
False && true
1
2
类型不合法的程序
3 + true
True + false
1
2
对这个语言,语义分析的任务是:对给定一个表达式e,写一个函数check(e)
返回一个表达式e的类型;若类型不合法,则报错
如3 + 4这个表达式返回int, 3+true这个表达式就会出错
下面对应抽象语法树的后序遍历的伪代码
enum type {int, bool};
enum type check (exp_t e)
{
switch(e->kind)
case exp_int: return int;
case exp_true: return true;
case exp_false: return false;
case exp_add: t1 = check(e->left);//左
t2 = check(e->right);//右
if(t1 != int || t2 != int) //根
perror(“error”);
else return int;
case exp_and: t1 = check(e->left);
t2 = check(e->right);
if(t1 != bool || t2 != bool)
perror(“error”);
else return bool
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
符号表
对于类型检查的有一个很重要的数据结构,符号表(key, value)
符号表:
用来存储程序中的变量相关信息
类型
作用域
访问控制信息(如pubic ,private之类的,文件之间的访问)
必须非常高效,因为程序中的变量规模会很大
如int x, bool y
table
x:int …..
y:bool ….
符号表要有创建,插入,查找的接口功能,具体实现和所选择语言相关
为了高效,可以使用哈希表等数据结构来实现呢符号表,
为了节约空间,也可以使用红黑树等平衡树
语义分析可能会出现的共性问题:
类型相容性:
类型检查问题往往归结为判断两个类型是否相等t1==t2? 在实际的语言中,这往往是个需要谨慎处理的复杂问题
如:
对采用名字相等的语言,可直接比较
对采用结构相等的语言(如结构体),需要递归比较比较各个域
面向对象的继承,需要维护类型间的继承关系
错误诊断:编译器要报出语义错误,这些信息要很清晰地给出
(1) 要给出极可能准确的错误信息
(2) 要给出尽可能多的错误信息
(3) 要给出尽可能准确的出错位置,程序代码的位置要从缱绻保留并传递归来(即词法分析的行号信息要传递到语义分析)
代码翻译:生成中间表示(或目标代码),现代编译器中的语义分析模块,除了做语义分析外,还要负责生成中间代码或目标代码,代码生成的过程也同样是对树的某种遍历
因此,语义分析往往是编译器中最庞大(代码量)最复杂(需要很多分析)的模块