如何写一个解释器(1):编译原理


最近在看DSL的东西,对于外部DSL,写一个解释器是必不可少的。我试图归纳一下我学到的,以写一个解释器为目标,
讲一下如果来实现一个可用的解释器。一个解释器通常可以分为一下几个阶段:

  1. 词法分析(Lexer)
  2. 语法分析(Parser, BNF, CFG, AST)
  3. 语义分析(AST的处理, annotated AST)
  4. 目标语言生成(stack-based)

这里的解释器不包括目标语言的执行和运行时环境,如果需要类似于python/ruby的解析执行器的话,还需要bytecode-compiler,
bytecode-interpreter和runtime。我们这里只谈解释的部分,不谈执行的部分。作为DSL我们大多都有宿主语言,通常也就我们生成的目标语言,
有关执行这部分,我们交给目标语言去解决。换个说法,我们只讨论一个完整解释执行器的前端。

当前编译技术已经得到极大发展,对于词法分析和语法分析,我们可以借助成熟的工具或库很快的完成,极大简化了写解释器的难度。
对于一个解释器来说我们需要自己处理的部分基本上集中于语义的分析,即AST的处理和目标语言的生成。
其实这里的AST的处理结果相当于一个与执行环境无关的中间语言表示,而目标语言的生成就是针对目标语言的特性,
将annotated AST转换为目标语言以方便在目标语言的运行环境中运行。

继续扯扯淡,Lisp语言的语法基本上就是直接写AST,直接用AST的表示法来表示程序,所以Lisp语法是最贴近于编译器的中间表示,
开始可能根本就不是给一般人用的,谁知道这么多人喜欢用。
Lisp之所以叫List Processer那是因为Lisp直接处理AST的数据结构表示即Nested List,所以才叫List Processor,一点都没有谦虚的意思。
在数据结构里,表示树这种数据结构的,最简单直接的办法就是Nested List(通过Linked-List了来实现)。
而且这种结构利于递归遍历,方便Stack-based执行或者代码的生成。

词法分析

词法分析是将源代码转换成tokens,这些tokens有些事关键字,有些是变量,有些是字面量,有些是语法结构,等等。比如:

var s = 5;

if(s > 0){

  print “greater than 5”

}

输出的tokens是[var, s, 5, ;, if, (, s, >, 0, ), {, print, “greater than 5”, }],这些tokens是有顺序的,但是没有具体的语法和语义。
我们的词法分析器就是将源代码作为输入,输出就是这个tokens的序列。

词法分析中我们会遇到一些问题,比如当我们看到whe的时候我们不知道这个是个变量名还是when关键字,这个需要看连续的4个字符才可以知道,
这个就是LL(4),LL代表从左边依次读取,从最左边的w->wh-whe->when依次来推导出这个是关键字还是变量。

这个我们这里不多讲,细节可以找本编译器的书来看看。有很多现成的工具和类库帮我们完成这样的工作,比如lex, flex, ply,等等。

语法分析

语法分析是帮我们分析这个token序列是否符合我们的语法,并且将语法分析结果用AST表示出来。既然是检查语法和按语法抽取出AST,
那么我们就需要表示语法,通常我们使用Backs-Naur Format,也就是我们常说的BNF表示法。不过好笑的是BNF本身是个规范,不是具体的实现。
也就是说同样BNF,不同的实现有不同的表示。比如:

statement ::= statement | empty

这个表示称为statement的产生式,也就是statement的语法规则,同样的有的BNF表示法是这样表示的

statement –> statement or empty

其中::=和->, |和or,是一样的意思。

语法分析的结果就是符合语法的AST,也可以说是符合语法的一个实例。

语法分析阶段我们也需要处理一些问题,比如为了解决算术的二义性,我们需要引入优先级(precedence),和结合性(associative)。

语法分析也有现成的工具和类库可以使用比如yacc, bison, antlr, ply等等。

语义分析

符合语法的源代码不一定有意义,比如a/0,这个就是没有意义的,这个我们需要在语义处理阶段解决。

待续…

published at April 28, 2014, 8 a.m.

Comments:

Sharing your thoughts: