跳至主要內容

语义分析(Semantic Analysis)

西风逍遥游大约 3 分钟

语义分析(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是一个函数,而不是一个变量,所以我们在语义分析中,就需要做相应的检查。

符号表

符号表的作用

语义检查的核心,主要依靠的是符号表。符号表是一个存储符号信息的数据结构,我们可以把符号表看成是一个字典,存储着每个符号的信息。符号表的作用是,当我们遇到一个符号的时候,我们可以通过符号表来查找这个符号的信息。根据上下文环境,来判断这个符号是否符合语义。

比如,我们在处理函数的参数时,就可以把参数名和参数类型存储到符号表中,在下面处理时就可以使用符号表来判断这个符号是否符合语义。

符号名符号类型类型信息
nametype definechar*
printffunction definevoid(const char*, ...)
sayHellofunction definevoid(const char*)

级联符号表

在实际的编译器中,我们会使用级联符号表来存储符号信息。什么是级联符号表呢?我们来看下面的代码:

void sayHello(const char* name) {
    if (name) {
        int age = 10;
        printf("Name: %s\n", name);
        printf("Age: %d\n", age);
    }
}

我们可以看到,我们会定义局部变量,而如果全局变量和局部变量重名的话,我们就需要使用符号表来区分这两个符号。我们可以使用一个符号表来存储全局变量,使用另一个符号表来存储局部变量。这样,我们就可以通过符号表来区分这两个符号了。

级联符号表很好的解决了这个问题,我们每个符号表都有一个父级符号表,当我们在当前符号表中找不到符号的时候,我们就会去父级符号表中查找。这样局部变量全部处理完后,我们就可以把局部符号表销毁,而不会影响到全局符号表。

全局符号表

符号名符号类型类型信息
printffunction definevoid(const char*, ...)
sayHellofunction definevoid(const char*)

第一层:函数符号表

符号名符号类型类型信息
nametype definechar*

第二层:if语句符号表

符号名符号类型类型信息
agetype defineint

如果有更多层嵌套,则可以继续添加更多层,整个符号表以栈的方式工作,进入一个作用域时创建,退出的时候,就可以把这个符号表销毁。