不知道大家有没有想过,某个编程语言的第一个编译器是怎么来的?这不是“先有鸡还是先有蛋”的问题吗?先说最后的结论:任何语言的第一个编译器一定是用其他语言写的。以我们嵌入式开发中经常用到的C语言为例,介绍一下第一个C语言编译器的源码。让我们回顾一下C语言的历史:1970年,Tomphson和Ritchie在BCPL(一种解释语言)的基础上开发了B语言,1973年他们又在B语言的基础上成功开发出了现在的C语言。在C语言被用作系统编程语言之前,Tomphson也曾使用B语言编写操作系统。可见,在C语言实现之前,B语言就已经可以投入实际使用了。所以完全有可能第一个C语言编译器的原型是用B语言或者B语言和PDP汇编语言混合编写的。但是B语言的效率比较低。如果全部用汇编语言编写,不仅开发周期长,维护困难,更可怕的是会失去高级编程语言所必需的可移植性。因此,早期的C语言编译器采取了取巧的做法:先用汇编语言为C语言的一个子集编写一个编译器,然后通过这个子集递归完成完整的C语言编译器。详细过程如下:先创建一个只有C语言最基本功能的子集,称之为C0语言。C0语言足够简单,直接用汇编语言就可以写出一个C0编译器。依托C0已有的功能,设计比C0更复杂,但还是不完整的C1语言,C语言的另一个子集,其中C0属于C1,C1属于C,用C0开发了C1语言的编译器.在C1的基础上,设计了C语言的另一个子集C2语言。C2语言比C1复杂,但仍不是完整的C语言。开发了一个C2语言的编译器……所以直到CN,CN已经足够强大了,这个时间足以开发出一个完整的C语言编译器实现。至于这里的N是多少,要看你的目标语言(这里是C语言)的复杂程度和程序员的编程能力。简单地说,如果你到了某个子集阶段,你可以很容易地使用已有的函数来实现C语言,那么你就找到了N。下图说明了这个抽象过程:这张图是不是很眼熟?对了,我用虚拟机的时候看到的,但是这里是CVM(CLanguageVirtualMachine),每个语言都可以在每个虚拟层上独立编译,除了C语言,每个层的输出都会作为输入下一层(最后一层的输出是应用程序)。这与滚雪球相同。用手(汇编语言)组合一小把雪,然后一点点滚下去,形成一个大雪球。这大概就是所谓的0生1,1生C,C生万物吧?那么这种大胆的子集简化方法是如何实现的,其理论依据是什么?首先引入一个概念,“自编译”(Self-Compile),即对于一些具有明显自举特性的强类型(所谓强类型是指程序中的每个变量都必须学会声明类型后才能使用被使用,比如C语言,相反,有些脚本语言根本没有类型)编程语言可以使用它们的有限子集通过有限的递归来表达自己,这样的语言包括C,Pascal,Ada等,至于为什么可以自编译,可以参考清华出版社的《编译原理》,它实现了Pascal子集的编译器。总之,有CS科学家已经证明,C语言理论上可以通过上述CVM方法实现一个完整的编译器,那么在实践中如何简化呢?下面是C99的关键字://一共37个autoenumrestrictunsignedbreakexternreturnvoidcasefloatshortvolatilecharforsignedwhileconstgotosizeof_Boolcontinueifstatic_Complexdefaultinlinestruct_Imaginarydointswitchdoublelongtypedefelseregisterunion仔细一看,有很多关键字帮助编译器优化,还有一些函数用来限制循环的范围(没有函数),这些不需要在编译器实现的前期加入,所以可以去掉auto、restrict、extern、volatile、const、sizeof、static、inline、register、typedef,从而形成C、C3语言的子集,关键字C3语言的用法如下://一共27个enumunsignedbreakreturnvoidshortcharforsignedwhilegoto_Boolcontinueif_Complexdefaultstruct_Imaginarydointswitchdoublelongelseunion再想一想,发现C3中其实有很多类型和类型修饰符。int就够了,所以进一步去掉这些关键字,它们是:unsigned,float,short,char(charisint),signed,_Bool,_Complex,_Imaginary,long,这样就形成了我们的C2语言,C2语言关键字如下://A一共18个enumbreakreturnvoidcaseforwhilegotocontinueifdefaultstructdointswitchdoubleelseunion继续想,即使只有18个关键字的C2语言,还是有很多高级的地方,比如基于基本数据类型的复合数据结构,而我们的关键字表中没有写运算符,复合赋值运算符在C语言中——>运算符如++、--等过于灵活的表达式此时可以完全删除,所以可以删除的关键字有:enum、struct、union,这样我们就可以得到C1语言的关键字:breakreturnvoidcaseforwhilegotocontinueifdefaultdointswitchdoubleelse//一共15个已经接近完美了,不过最后一步自然要大一些。这时,数组和指针也会被移除。此外,C1语言还有很多复杂性。例如,控制循环和分支的表达方式有很多种,其实可以简化为一个,具体来说,循环语句有while循环、do...while循环和for循环,保留while循环即可;分支语句有if...{},if...{}...else,if...{}...elseif...,switch,这四种形式可以用两个以上的if来实现。..{},所以只要保留if,...{}就足够了。但是再想一想,所谓的分支和循环只是条件跳转语句,而函数调用语句只是压入和跳转语句,所以只需要goto(无限制的goto)即可。因此,大胆去掉所有的结构关键字,连函数也不行,得到的C0语言关键字如下://一共5个breakvoidgotointdouble这就是极致的简洁。关键字只有5个,用汇编语言可以快速实现。通过逆向分析,我们还原了第一个C语言编译器的编写过程,也感受到了前辈们的智慧与辛劳!我们不过是巨人肩上的尘土!0生1,1生C,C生万物,真是巧妙啊!任何语言的第一个编译器一定是用其他语言编写的。那么就有一个问题:第一语言的第一个编译器是怎么来的?哈哈!
