我相信有不少的人对编译器的有很大兴趣,但是虎书(《编译原理》)上的理论知识虽然很全面很详细,但是相当的枯燥无味,让人难以下决心钻研。我就是被虎书吓坏了,各种看不懂(本人非CS专业,全靠自己啃)。。。《程序语言设计》也太厚了,后来在图书馆找书的时候,发现了一本适合入门PLT的书籍,拥有基本的C语言、操作系统和数据结构的功底就能完全看懂的书籍。这本书叫《自己动手写编译器链接器》,建议想了解PLT的可以认真钻研,可以对编译原理有个初步的概念。大概花了两个月的时间(边实习边学习)完成了这个玩具,编译器的大部分核心是来自《自己动手写编译器链接器》这本书,自己也加入了一些其他的想法,另外本文分为几篇完成,主要给一些摸索中的人提供一些资料,也是对自己的一个总结,如果有不正确的地方还请见谅和指点。github地址(https://github.com/Nevermore1994/ViaC)。
注意:本文是进行设计和源码上的大致总结,不是详细的解读源码,也不是手把手从头写起。如果需要深入了解源码,请阅读书籍。另外本文将在结尾总结处,给出搜集的编译原理、编译器相关的资料。
好了,介绍下这个玩具,主要采用了C语言和C#为开发语言,XML为文件配置语言,开发工具为VS2015,设计的语言是C语言的一个子集,支持C语言的大部分,支持项目和单文件的编译(当然很简陋的)。C语言是写编译内核的,C#主要写UI,显而易见是Windows平台了,下面进行详细介绍。
目前业余时间正在开发一个新的编译器学习项目Tlang, 由于目前工作时间原因,进度比较慢,欢迎大家一起加入开发这个项目,打造出一个完整编译器,成为一个学习编译器的优质开源项目。
1.语言的描述和定义
Via-C是C语言子集,采用的是ASCII序列。其定义在C语言(C89)的基础上,简化了C89的一些细节,加入了其他的一些功能。Via-C不同于C语言必须使用分号分隔语句序列,也支持脚本格式的风格,无需行结束符。
可以在上表中看出,同C相比多了几个关键字,其余大多数意义是类似的。其中do和end是代表代码块开始和结束,可以和{}(大括号运算符)一起使用。require时用来载入头文件,和include一致。
C89的规定中只是支持了代码块注释方法,Via-C添加了注释行的功能。其他和C语言基本是一致的,如函数、表达式或者是标识符等等,但不支持宏定义。下表解释了Via-C语言的运算符的优先级。
另外Via-C编译器的独立文件格式为.viac,项目文件格式为.viacproject。
2.编译器的定义
通常编译器的逻辑层次可以分为前端和后端。前端可以分为词法分析器等部分,后端包括了代码生成器和中间代码优化等部分。记住链接器并不属于编译器,这个小玩具也加入了链接器。本玩具(以下称Via-C编译器,原谅我不要脸)分为了词法分析器、语法分析器、语义分析器、代码生成器和中间代码优化、链接器、UI周边功能,这六大部分分别承担不同的任务,各部分独立又各有联系(见下图,来自《自己动手写编译器链接器》)。下面将根据层次逻辑顺序逐一进行介绍。
2.1词法分析器
词法分析是逻辑顺序结构的第一步。任何的代码都是字符按照一定的顺序排列而成,当扫描完程序后,需要凭借相关规则进行分割,形成独特的记号(类似于单词),再将辨别的记号经过编码形式输出。但是词法分析器不保证单词之间的任何关系。词法分析器流程如图。
词法分析器流程图
以下进入相关的实现步骤。
2.2单词编码
源程序中可能有许多的单词,这时就需要单词编码来进行管理。单词编码采用的是枚举类型,这样在查看和使用编码信息会更加便利。Via-C语言的单词编码对关键字和运算符都进行了编码,详情见代码中的lex.h中的enum e_TokenCode类型。
2.3建立数据结构
在进行词法分析时需要大量的数据结构,在进行词法分析之前需要先准备基本的操作,故定义了下列几种数据结构。
采用哈希表是由于在分析的时候中需要使用大量查询操作。哈希表需要哈希函数去计算哈希值得到关系,哈希函数的作用是将关键字的集合经过某种运算映射至某个地址的集合上。在本设计中使用了UNIX系统中的ELF_Hash函数,具体实现见compile.h文件。哈希函数实现如图所示:
哈希函数的定义
Via-C语言是以连续的字符流表示,需要大量的字符串处理,而且单词的长短不一,自定义一个动态字符串来进行字符串处理是有必要的。动态字符串定义如下:
typedef struct String
{
int count; //字符串长度
int capacity; //字符串缓冲区
char* data; //字符指针
}String;//(见string.h)
并且定义了一系列的操作函数见(string.h)。
动态字符串操作函数
单词的个数是无法预知的,为了满足后续操作的空间内存的需要,也建立了动态数组的定义。动态数组定义如下:
typedef struct Array
{
void** data; //指针数组
int count; //元素个数
int capacity; //缓冲区
}Array;//(见array.h)
动态数组操作函数如图。
动态数组操作函数
下面介绍单词表的定义,单词表是由动态数组和哈希表复合构成。
typedef struct TkWord
{
int tkcode; //单词编码
struct TkWord* next; //哈希冲突的同义词
char* spelling; //单词字符串
struct Symbol* sym_struct; //单词的结构定义
struct Symbol* sym_id; //单词的标识符
}TkWord;
单词表初始化时对关键字和运算符进行了特殊处理,使得它们提前进入单词表。在词法分析程序中能快速查询到一个引用在内存中的位置,进行引用。
下图是单词表的一系列操作函数的声明。
单词表操作函数
2.3词法分析器控制程序
以上完成了相关的预备工作,接下来开始控制程序的介绍。
下列是字符读取函数:
void GetCh(){
ch = getc(fin); //fin为文件描述符
}
读取字符后,需要在单词表中识别是否为单词表成员。因此接下来是单词分割程序,主要识别运算符、变量、关键字,函数名为GetToken(见lex.c)。
预处理程序主要是忽略注释和空白字符、换行符等(见lex.c)。
剩下皆是一些解析函数和词法着色程序,基本上完成了该阶段逻辑的编写工作,见lex.c。
2.4词法分析器的测试
进行单元测试,以下图是词法分析的成果。
词法分析成果
从上图可以看出对关键字、标识符、字符串的分别着色,说明词法分析器工作正常,Via-C编译器的第一阶段分析大功告成。
今天就写到这吧,明天继续。以上的代码均可在github上。