之前写了12篇使用有限自动机(DFA)分析语法的文章,今天说一下语义分析。
怎么用C语言写语法分析3,基于有限自动机的表达式分析
怎么用C语言写语法分析
用C语言实现一个真正的词法分析器
语义分析,也是编译器前端的一个模块。
一般来说,它比语法分析要简单。
程序源码的各种复杂逻辑,在经过语法分析之后,就成了一棵抽象语法树(AST)。
生成这个语法树比较难,因为源代码里互相耦合的概念太多。
生成语法树之后,把树遍历一次,就可以完成语义分析。
不同的语句和运算符,构成了语法树的不同节点。不同的节点有不同的处理函数。在处理函数里检查类型是否一致、计算中间结果、化简常量表达式、添加类型的自动转换(type cast)。
之后,就可以生成三地址码了。从生成三地址码开始,已经是编译器的后端了。
如上图,是语法树的节点数据结构。
因为是多叉树,所以有个父节点的指针parent,子节点组成一个指针数组,由nodes和nb_nodes表示。
31行的scf_operator_t* op,表示节点的操作符。不管是语句还是运算符,一律看作一种操作符。
把所有的操作符组成一个大数组,就是支持的所有语义。
(把人能看懂的源码文本,终于变成了机器能处理的数据结构了)
第1列,用一个整数表示操作符的类型。
第2列,是操作符的名字字符串。
第3列,是运算优先级。
小括号表示的表达式,优先级最高的,语句的优先级最低。先乘除,后加减。
第4列,是操作数(子节点)的个数。双目运算符是2个,单目是1个,语句的子节点个数不确定(设置为-1)。
第5列,是结合性。大多数都是左结合,单目运算符和赋值运算符是右结合。
整个语义分析,分多步处理,每一步给它设置一个处理函数句柄。如下图:
type,就是操作符的类型。
func,就是处理函数的指针。
第8行的最后一个参数void* data,是处理函数需要的私有数据。
把每一步的处理函数句柄也组成一个大数组,如下:
最后一列,就是每类节点的处理函数。
语义分析比语法分析简单,因为语义分析是个填空题,语法分析是个问答题。
想多支持一个运算符,就在这里填个空就行(笑)。不像语法,多加个符号就可能出现二义性了。
接下来,以表达式的语义分析做个例子:
表达式只有一个子节点,这个子节点的result字段是变量(scf_variable_t类型的指针),用于存放计算的中间结果(实际在生成机器码的情况下,主要是记录中间结果的类型)。
它调用_scf_expr_calculate_internal()函数,递归处理表达式。
递归结束的时候,叶子节点有可能是变量,也有可能是标号(goto 语句)。
编程语言里的表达式,是扩展了的表达式,比四则运算复杂一点。
如果不是叶子节点,那么就一定是操作符(运算符或语句)。
417-426行,如果有子节点,就递归处理。
如果是左结合,子节点的处理顺序从0 ~ nb_nodes - 1。
如果是右结合,子节点的处理顺序从nb_nodes - 1 ~ 0。
处理完子节点之后,再处理父节点。语法树上,父节点的运算优先级肯定比子节点低。
处理父节点时,要先查找它当前步骤需要的处理函数句柄,然后调用。
实际上,语法树的处理,类似一个简单的计算器。
在简单计算器的代码上,稍微修改一下就可以做语义分析。
难写的还是语法分析,从源码开始生成这个语法树。
再看看加法的语义分析:
加法是双目运算符,分析代码可以和减法通用。
首先,获取它的两个操作数变量:v0和v1。
然后,如果这两个变量至少有1个是结构体或类的指针,那么按运算符重载处理。
如果没有定义重载函数,则当作普通指针的加减运算。
不是类的对象指针的情况下,就只能是整数或浮点数。
指针,也属于无符号整数。
如果第一个变量是指针,那么第二个变量要么是同类型的指针,要么是个整数。否则,报错类型不一致。
类型检查,都在这个阶段处理。
如果可以自动升级,就自动升级(例如32位整数扩展到64位)。
指针运算的结果还是指针,结果类型与第一个变量一致。包括是否为const指针。
如果第二个变量是指针,第一个变量就要是整数,与上面的类似。
如果两个变量类型相同,不管它们是什么类型,都是合法的运算。
如果类型不同,则自动类型升级,查找一个可以升级的类型。找不到则报错,提示程序员自己处理。找到的情况下,自动添加type cast节点。
最后,生成一个结果变量,用于记录运算结果的类型。
if语句的语义分析:
if语句的条件表达式,计算结果必须是一个整数(0表示false,非0表示true)。
如果计算出浮点数来,一般表示程序有BUG,可以直接报错。
然后分析if的主体部分,主体部分有可能嵌套各种语句类型,递归分析。
实际上,语义分析是特别无聊的代码,很多运算符的代码都是大同小异。
想了解更多精彩内容,快来关注闲聊代码