我的编译器 开篇

来源:互联网 发布:lovelivesunshine知乎 编辑:程序博客网 时间:2024/04/30 22:16
引言
       我在一个数据库项目的开发过程中,需要一个窗体编辑器,待到有了一个拙劣的实现之后,我突然发现我需要一个解释器!按照我自己的想法,我写了一些代码,但很快发现这不是一个解决之道。我需要的是一个真正的解释器。于是我便开始进行文本分析方面的研究,在基础的理论中,当我接触到EBNF这个东西的时候,我惊喜的发现,我要找的东西就是这个了——只要实现了一个EBNF的编译器,就可以通过书写EBNF代码描述目标语言,从而生成目标语言的解释器。进行了一段时间的使用后,发现Yacc(一个已有的生成分析器C的代码的工具)的语法非常晦涩,让我这个编译器的新手感到非常不适。于是,我开始了创建自己的EBNF方言的路途。不断地在黑暗中探索,并最终有了这篇文章所描述的一点成果。
 
 
 
简要说明
       自动分析工具用于编写语言的分析器,用EBNF语言编写2型语言的语法,然后通过自动分析工具生成相应的C代码文件(传统的自动分析器工具大部分生成C代码,这主要是因为大部分编译器使用C/C++编写),然后链入编译器其它部分的源代码,一同编译,之后生成完整的编译器。常见的自动分析工具有使用LR分析法的YACC和使用LL的Lex,由于它们都生成C代码,需要再次编译,并且使用的EBNF的语法偏向于C的风格,对于专家级的编译器制作人员,这也许并不成什么问题,但是对于其他非编译器制作者,要使用一个解释器或者一个微型的编译器,学习C风格的EBNF语法显然比较费事,而VB的接近自然语言的语法则对于初学者有着非常大的吸引力。于是我就有了制作了这么一个接近VB语法的EBNF的编译器/解释器。使用更接近于自然语言的语法来降低对于编译器/解释器的制作难度。
 
       首先让我们来了解一下上面提到的EBNF的一些细节。EBNF是在老的BNF的基础上进行一些扩展而成的,它相对于BNF则更接近于程序的逻辑,如循环、条件。BNF即Backs-Naur范式,是由此二人提出的用于描述2型语言的一种形式语言。使用这种语言来编写语法统一的语法说明文件不仅有助于交流而且使构造一个描述语言的语言成为了可能。而我们所要做的工作就是构造一个这样的编译器,同时开发一个使用它的编译器,以证明其可用性。
       由于YACC的语法非常晦涩,所以我并不打算使用它的语法,所以接下来我们就要定义一个EBNF的方言来达到我们的目的。首先要考虑的问题就是语法。使用一个什么样的语法?当然现在开始就定义一个比较复杂的语言对于我们没有任何好处,原因也是一样的显而易见:由于初期我们只能使用手工编码的方式,一个复杂的语法必然大大增加实现的复杂性,而语法文件与EBNF编译器到底是谁出的错也将很难查找,从而不如一开始就只定义一个使用到大概所有EBNF编译器的功能的语言,越简单越好。由于主要是语法规则的问题,所以在简单性上,加入开始符号和结束符号无疑是一个很好的方法。当然语法接近VB将对学习此语言是很有帮助的。于是初期的想法当然就是:
       Section XXXXX
              Rule XXX=>....
       End Section
       这种语法非常接近于VB,同时符号也非常明显(例如推导符=>)。而且它的语法是递归的,对于构造这样一个编译器也非常简单。
       这样在扫描器(本章所指扫描器、分析器等均为EBNF编译器的)遇到Section的时候就可以开始取区段名,然后开始语法规则的分析工作。而对于语法规则,在使用回车还是分号作为逻辑行结束符的问题上,最初采用与VB一样的回车作为结束符,但是在后来的使用中逐渐发现有些语法规则非常长,这样如果使用回车作为逻辑行结束符会使一个行变得非常长,而加入另外的换行符无疑将增加难度,于是在后继版本(1.1~1.5)中,使用了分号作为逻辑行结束符,同样带来的问题就是空格不再是有效的元字符,而成为不折不扣的分隔符。
       扫描器使用的是3型语言,分析器使用的是2型语言。既然2型语言可以描述3型语言能描述的所有的语言(具体为什么可以参见编译器的教科书),所以就不如直接使用2型语言来描述扫描器,只不过对使用作一些限制。这样就能够大大简化实现方面的工作(阅读代码时就能够体会到这些限制使得扫描器的构造充分利用了.NET的类库使代码大幅简化)——因此只需实现一个2型语言的编译器。
       另外一个不能忽视的设计问题就是,如何使用这个编译器?将语法直接编译成为原代码?.NET的程序集?还是编译成为一种内部结构,然后通过序列化保存成文件?客观上讲,直接编译成为.NET的程序集是最好的,管理上方便,使用上速度也可能比较快。但是起决定作用的却不是这一点,因为如果要编译成为程序集的话需要将依此为驱动的扫描器和分析器都集成在其中。想一想,我们现在根本什么也没有!无法集成任何东西,我们还要它干什么?编译为源代码也有同样的问题。于是最后的方案就是使用序列化的内部表示。使用时再反序列化为内部的表示,然后构造一个通用的扫描器和分析器或者再加上一个适配器来使扫描器和分析器工作起来。而且语法规则和分析器/扫描器可以分别调试,对于现在这种处于研究阶段的项目来所也是非常合适的。
       下面就是C3的定义及自描述,这是经过数次修改之后的1.4版(最初能用的那个版本称为 1.0版)。
原创粉丝点击