标签:
本系列的第一篇,我想概述一下编译器的构造,同时帮助大家了解编译器中各个组成部分的用途。想必大家看别的编译原理书籍,大都在第一章或者序言之类的地方,将编译器分成许多模块,然后每一个模块负责编译的特定阶段,最后串起来组成完整的编译器。比如下面这张图就是虎书(Modern Compiler by Andrew W. Appel)第一章中出现的编译器阶段示意图:
那么,为什么要将编译器拆成一个个阶段,一个个模块呢?答案是,为了更加容易设计和理解。一个完成编译器怎么也算是一项大工程,如果不将其分解,将是非常难以编写和维护的。而编译器的模块划分得越清晰,工作就越简单。比如在词法分析阶段将输入的字符流转化成单词(token)流,就大大减少了语法分析阶段需要判断的输入种类,在简化设计的同时还有助于提高性能。此外模块化还将编译器各个阶段的工作尽量独立开。比如编译器可以进行与具体CPU无关的优化,也可以针对某种CPU进行特定的优化,都可以分别独立进行而不用重新设计整个系统。
有个事实可能会令人感到惊讶,编译器的各个阶段和模块如何设计,甚至跟这种编程语言的语法有关。比如早期的编程语言Fortran,在设计当初人们还没有掌握现在这么多编译原理的理论,它的语法就不能像当今的语言一样清晰地分成词法分析和语法分析等阶段。因为Fortran的语法并不包含可以用自动机独立处理的词法结构。于是,Fortran语言的编译器在语法分析方面就比较繁杂。有一些历史背景的语言也可能会具有这种复杂化的语法,比如Visual Basic也属于不能用独立的、基于自动机的词法分析器来扫描的语言。因此VB的语法分析器就要比诸如C#等思路较新语言的难写很多。另外一个例子是早期的Pascal语言和某些C语言允许用一些特定的语法来指定某变量为寄存器变量(也许在近期的Delphi中仍然存在,求证)。这是因为当时还没有非常有效的寄存器分配算法,需要程序员凭自己的经验来决定。在今天如果一种语言还允许显式指定某个变量是寄存器变量,就会干扰寄存器分配模块的设计。综上所述我想给各位未来的编译器设计师们一个建议,好好设计你们的语法,就能大大简化编译器的设计!
除了简化设计之外,将编译器的各个阶段模块化还有更大的价值。原先我们认为编译器只要把源文件编译成最终的目标代码就好了。但是随着各种各样的开发工具出现——编辑器、自动完成、调试器、重构工具、测试覆盖率检测、性能剖析器…… 人们发现编译器编译过程中,各个阶段产生的结果都可能是非常有价值的。将编译器内部结构和中间结果暴露给用户是必然的趋势。比如Visual Studio下一代产品中将提供的Compiler as a Service特性,其做法就是将编译器的内部模块暴露给用户成为一种服务。我举几个例子可以让大家看到编译器模块的输出有哪些可能的用途:
编译器的阶段 |
产生的结果 |
用途 |
词法分析 |
单词流 |
语法高亮 |
语法分析 |
抽象语法树 |
语法高亮;代码格式化;代码折叠 |
语义分析 |
带类型信息和符号表的抽象语法树 |
重命名;重构;代码自动生成;代码自动改写 |
数据流分析 |
控制流图、冲突图 |
编辑后继续运行(Edit and Continue) |
这里我只是举几个简单的例子,以上结果的用途当然不会仅限于此。我相信将编译器的内部模块暴露给用户还能产生无数有趣和有价值的应用。
上述编译器的各个阶段还可以根据其用途分成两个大阶段:词法分析、语法分析和语义分析重点在处理编程语言的符号系统上,统称为编译器的前端(front-end),而中间代码生成、规范化、指令选择、控制流分析、数据流分析、寄存器分配、指令流出、汇编、连结等着重处理代码计算逻辑的阶段统称为编译的后端(back-end)。应该说现代编译器研究的工作重点是编译器的后端,因为前端的技术已经相对非常成熟。但是前端的技术对我们日常开发来讲可能更有机会用到,而且通常更具趣味。所以我也会花较多时间在前端技术上。当大家完成一种编译器的前端后,有几种实现后端的选择:
我将展示的例子miniSharp虽然是C#语法的子集,但是并没有限定必须运行在CLR之上。我会将它设定成一个可重定向的语言,即可以针对多种后端。这样就可以用一个例子演示尽可能多的技术。我也会视我自己的能力范围和工作进度动态调整本系列的内容。也希望大家继续关注VBF.Compilers项目(https://github.com/Ninputer/VBF)和我的微博(http://weibo.com/ninputer)!敬请期待下一篇。
标签:
原文地址:http://www.cnblogs.com/Kevin-Bruce/p/4306766.html