语义分析(Semantic Analysis)
语义分析(Semantic Analysis)
什么是语义分析呢?我们来看看如果从语法层看下面两段代码:
void sayHello(const char* name) {
if (name) {
printf("Hello World, %s!", name);
}
}
以及
void sayHello(const char* printf) {
if (name) {
printf("Hello World, %s!", sayHello);
}
}
我们会发现我们明显看不懂下面这段代码在干什么,当然,因为这是我乱写的。
我们可以看到,这两段代码的语法都是正确的,但是第二段代码的语义是错误的,因为我们在第二段代码中,把printf
当做了一个参数名,而不是一个函数,sayHello
明明是函数名,却又被当成了参数。检查语法树的内容是否符合语义,这就是语义分析的工作。
符号
在语义分析中,我们核心要处理的就是符号。那么什么是符号呢?你可以简单把代码中,所有你能自己定义名字的东西都理解成是符号,比如变量名、函数名、类名、结构体名、枚举名、宏名等等。这些都是符号。我们在语义分析中,要做的就是检查这些符号是否符合语义。比如,我们在上面的例子中,printf
是一个函数,而不是一个变量,所以我们在语义分析中,就需要做相应的检查。
符号表
符号表的作用
语义检查的核心,主要依靠的是符号表。符号表是一个存储符号信息的数据结构,我们可以把符号表看成是一个字典,存储着每个符号的信息。符号表的作用是,当我们遇到一个符号的时候,我们可以通过符号表来查找这个符号的信息。根据上下文环境,来判断这个符号是否符合语义。
比如,我们在处理函数的参数时,就可以把参数名和参数类型存储到符号表中,在下面处理时就可以使用符号表来判断这个符号是否符合语义。
符号名 | 符号类型 | 类型信息 |
---|---|---|
name | type define | char* |
printf | function define | void(const char*, ...) |
sayHello | function define | void(const char*) |
级联符号表
在实际的编译器中,我们会使用级联符号表来存储符号信息。什么是级联符号表呢?我们来看下面的代码:
void sayHello(const char* name) {
if (name) {
int age = 10;
printf("Name: %s\n", name);
printf("Age: %d\n", age);
}
}
我们可以看到,我们会定义局部变量,而如果全局变量和局部变量重名的话,我们就需要使用符号表来区分这两个符号。我们可以使用一个符号表来存储全局变量,使用另一个符号表来存储局部变量。这样,我们就可以通过符号表来区分这两个符号了。
级联符号表很好的解决了这个问题,我们每个符号表都有一个父级符号表,当我们在当前符号表中找不到符号的时候,我们就会去父级符号表中查找。这样局部变量全部处理完后,我们就可以把局部符号表销毁,而不会影响到全局符号表。
全局符号表
符号名 | 符号类型 | 类型信息 |
---|---|---|
printf | function define | void(const char*, ...) |
sayHello | function define | void(const char*) |
第一层:函数符号表
符号名 | 符号类型 | 类型信息 |
---|---|---|
name | type define | char* |
第二层:if语句符号表
符号名 | 符号类型 | 类型信息 |
---|---|---|
age | type define | int |
如果有更多层嵌套,则可以继续添加更多层,整个符号表以栈的方式工作,进入一个作用域时创建,退出的时候,就可以把这个符号表销毁。