这学期有编译这门课。这门课甚是无聊,我大约翘了一半。课程包含一个Project,任务是分三次来完成一个编译器。和其他课程的Project一样,起初,我想随便灌灌水就过去。可是,一天早上起来,我突然想到了一个编译器的框架——在这个框架下,仅仅修改一部分配置文件就能实现一门语言的编译过程。于是,从上周六开始,我着手实现了编译框架的各个部分(包括词法分析、语法分析、中间代码生成与中间代码解析执行)。说实话,看着编译框架的一个个部分逐渐实现并正常运行,心里还是很激动的。我回想,这可能是大学时期自己做过的最有意思的课程Project了。
我最早的想法很简答——编程者都会在编写代码的过程中产生自己对于编程语言的看法。比如,在PASCAL中,函数的定义是这样的:
<type> <function name> ( <variable 1> : <variable type 1>, <variable 2>: <variable type 2>, ... )
在C语言中,函数的定义是这样的:
<type> <function name> ( <variable type 1> <variable 1>, <variable type 2> <variable 2>, ... )
当然,两种方法仅仅是语法上的差别,但是,不同的编程者可能会有偏好(我个人喜欢后一种)。
尽管编程语言在语法上会有差别,但只要是面向过程(或函数式)的语言(面向对象的语言模型就大大不同了),其语言的特性都是类似的,无非是以下几条:
- 顺序执行
- 条件分支
- 循环
- 函数调用
那么,对于这些语言,语法不那么重要——无论何种语言,都可以对应到一套预定义的中间代码上面去。对应的过程很简单——仅仅需要将代码的每一种规约方式(比如,if语句的某一种归约方式可以为“if (判断){执行}”)翻译到中间代码就可以了。依然以“if (判断){执行}”的语句为例,假设这句语句的形式是“W(X){P}”,其中,W代表if,X代表一判断语句,P代表一代码片段,那么,这句语句可以这样对应到中间代码:
- STEP 1:生成X对应的代码
- STEP 2:根据STEP 1的结果,如果结果为1,跳转到STEP 4
- STEP 3:生成P对应的代码
- STEP 4:(空)
实践证明,任何一个类似于上述if语句的归约方式,都可以翻译到一个仅仅包含了17个语句的中间代码中(这17个语句是我自己定义的,有可能还可以减少)。
那么,问题就变成了这样——只要一种语言的语法可以用LR Parser(http://en.wikipedia.org/wiki/LR_parser)、LALR Parser(http://en.wikipedia.org/wiki/LALR_parser)等parse算法接受,并且,这种语言的上下文关联性不是很强,这门语言就可以被这个被编译框架接受。当然,如果语言的关联性很强,比如支持面向对象特性,中间代码的生成就可能需要分多次执行(像gcc的link过程一样),这类语言的翻译过程我还没有想得特别清楚。
下一步,对于只包含了17个语句的中间代码,解析执行就十分简单了。其中需要注意的几点是:
- 变量的重复声明,或者未声明使用,都是需要判断的
- 每个变量都有自己的定义域,出了定义域,变量就失效了
- 函数调用可以是递归的,需要一个函数调用的栈来处理
当然,这几点实现起来都不是很难。至于变量的定义域,我特地在中间代码中加入了SCOPE和ENDSCOPE两个语句——这样,在解析执行时,什么时候要申请变量以及什么时候要销毁变量就变得简单、直接了。
以上是对这个编译框架的简单描述。有空我再说得具体一些。
项目的代码我已经上传到GitHub(https://github.com/davidsun/TinyCompiler),欢迎围观。