zcc-编译器开发总结-part1-前言

注意:本系列文章是 zcc 编译器 总结系列文章,本系列文章并不打算呈现所有的代码细节(主要是细节太多了,全部呈现既不现实文章也看着冗长而没有意义)。因为是总结性质的文章,所以更多是分享实现的过程中个人感兴趣的点

编译器大体流程

大体流程都比较简单,其实就是经典的 3 个步骤

  1. 词法分析
  2. 语法分析
  3. 语义分析
  4. 生成汇编代码
  5. 初始编译器编译链接执行

词法分析:也就是写一个分词/扫描器,对整个要编译的文件进行扫描,将一些特殊的 token 比如符号或者数字等提取出来,然后将它们置为各种各样的 state,方便接下来的处理

语法分析:这个时候有了上面的词法分析,可以对整个语句的 语法结构 进行分析了,比如整个语句要不要加分号,if 语句后面要不要接小括号之类的,整个语法应该怎么来,不符合语法的就报错

语义分析:所谓语义,就是语句中实际的含义,而这个含义必须是 绝对明确 的,比如 int xxx = 1;,它的 语义 就是 把一个 32 位的整型常量存储到变量 xxx 中,那么 1 这个有被明确定义成所谓的 32 位的整型常量 吗?语义分析部分要做的,就是确定这些东西的 类型,即所谓的 类型检查。这也是语义分析贯穿始终的一个步骤。而判断的点基本有

  1. 判断表达式/变量的声明类型
  2. 判断变量赋值时是否有隐式的转换
  3. 如果有隐式转换则转,没有则报错

生成汇编代码:这个没什么好说的,也算是编译器 back-end 的工作了,基本上在 zcc 的代码实现中,就是基于语法分析和语义分析之后生成的 ast,生成对应的汇编代码。然后再让最初始的编译器,编译这段汇编代码,然后执行

那么 zcc 主要做了哪些事情

  1. 有一个 scan.c,这个就是用来扫描代码文件并生成对应的 token,同时对每个扫描到的 token 生成对应的 state
  2. 有一个 parser.c,主要根据 token 将语句拆成一个个的 node,然后将这些 node 组装成 一个 token 类型的 ast
  3. 有一个 statement.c,主要根据关键字比如 if/while/return 等等把对应的语句组装成一个 ast 类型的 ast
  4. 有一个 declaration.c,这个算是最重要的文件,解析和判断各种语句的声明定义,并根据解析好的 ast 生成汇编代码
  5. 有一个 generator.c 以及 generator_core.c,用于生成汇编代码的工具函数文件

那么 zcc 是严格按照编译器大体流程来的吗,是的,也不全是,似乎在大多数人的印象中,这几个步骤都是先对一整个代码文件进行分析,分析完成后再进行后面的操作,比如先把代码文件分析成 token,然后后面再基于这个操作进行语法和语义分析。诚然这也是编译器的一种写法,然而本 zcc 编译器则是 分析一小段代码 -> 解析成 token -> 生成这部分的一个 ast 节点 -> 再分析一小段代码 -> 解析成 token -> 然后和上一个 ast 节点组合生成一个新的 ast 节点 这样的一种循环,直到对 main 函数的解析结束(这个时候分析到文件结尾了),文件的全部 ast 已经组装完成,这个时候再对这部分 ast 生成对应的汇编。

这里引用《编译原理之美》的一张图,解析 int age = 45; ...,可以表达我上面所说的情况

所以相对于前面的那种方式而言,在编写编译器的过程中代码组织会相对简单(目前暂不考虑所谓的性能问题)

本系列文章总结的主体内容

目前的规划如下

  1. zcc 编译器的 front-end 端有哪些值得注意的点
  2. zcc 编译器的 back-end 端的一些原理
  3. zcc 编译器的 middle-end 端如何实现的(对的,本编译器实现了一个比较简单的优化过程)
  4. zcc 编辑器是如何将文件 链接 起来然后编译的
  5. 展望一下编译器还能继续做哪些事情(可能会写)

大致就这些