注意:本系列文章是 zcc 编译器 总结系列文章,本系列文章并不打算呈现所有的代码细节(主要是细节太多了,全部呈现既不现实文章也看着冗长而没有意义)。因为是总结性质的文章,所以更多是分享实现的过程中个人感兴趣的点
编译器大体流程
大体流程都比较简单,其实就是经典的 3 个步骤
- 词法分析
- 语法分析
- 语义分析
- 生成汇编代码
- 初始编译器编译链接执行
词法分析:也就是写一个分词/扫描器,对整个要编译的文件进行扫描,将一些特殊的 token
比如符号或者数字等提取出来,然后将它们置为各种各样的 state
,方便接下来的处理
语法分析:这个时候有了上面的词法分析,可以对整个语句的 语法 和 结构 进行分析了,比如整个语句要不要加分号,if 语句后面要不要接小括号之类的,整个语法应该怎么来,不符合语法的就报错
语义分析:所谓语义,就是语句中实际的含义,而这个含义必须是 绝对明确 的,比如 int xxx = 1;
,它的 语义 就是 把一个 32 位的整型常量存储到变量 xxx 中,那么 1
这个有被明确定义成所谓的 32 位的整型常量 吗?语义分析部分要做的,就是确定这些东西的 类型,即所谓的 类型检查。这也是语义分析贯穿始终的一个步骤。而判断的点基本有
- 判断表达式/变量的声明类型
- 判断变量赋值时是否有隐式的转换
- 如果有隐式转换则转,没有则报错
- …
生成汇编代码:这个没什么好说的,也算是编译器 back-end 的工作了,基本上在 zcc
的代码实现中,就是基于语法分析和语义分析之后生成的 ast,生成对应的汇编代码。然后再让最初始的编译器,编译这段汇编代码,然后执行
那么 zcc 主要做了哪些事情
- 有一个
scan.c
,这个就是用来扫描代码文件并生成对应的token
,同时对每个扫描到的token
生成对应的state
- 有一个
parser.c
,主要根据token
将语句拆成一个个的node
,然后将这些node
组装成 一个token
类型的 ast - 有一个
statement.c
,主要根据关键字比如if
/while
/return
等等把对应的语句组装成一个ast
类型的 ast - 有一个
declaration.c
,这个算是最重要的文件,解析和判断各种语句的声明定义,并根据解析好的 ast 生成汇编代码 - 有一个
generator.c
以及generator_core.c
,用于生成汇编代码的工具函数文件
那么 zcc 是严格按照编译器大体流程来的吗,是的,也不全是,似乎在大多数人的印象中,这几个步骤都是先对一整个代码文件进行分析,分析完成后再进行后面的操作,比如先把代码文件分析成 token
,然后后面再基于这个操作进行语法和语义分析。诚然这也是编译器的一种写法,然而本 zcc 编译器则是 分析一小段代码 -> 解析成 token -> 生成这部分的一个 ast 节点 -> 再分析一小段代码 -> 解析成 token -> 然后和上一个 ast 节点组合生成一个新的 ast 节点 这样的一种循环,直到对 main
函数的解析结束(这个时候分析到文件结尾了),文件的全部 ast 已经组装完成,这个时候再对这部分 ast 生成对应的汇编。
这里引用《编译原理之美》的一张图,解析
int age = 45; ...
,可以表达我上面所说的情况
所以相对于前面的那种方式而言,在编写编译器的过程中代码组织会相对简单(目前暂不考虑所谓的性能问题)
本系列文章总结的主体内容
目前的规划如下
- zcc 编译器的 front-end 端有哪些值得注意的点
- zcc 编译器的 back-end 端的一些原理
- zcc 编译器的 middle-end 端如何实现的(对的,本编译器实现了一个比较简单的优化过程)
- zcc 编辑器是如何将文件 链接 起来然后编译的
- 展望一下编译器还能继续做哪些事情(可能会写)
大致就这些