我们需要实现一门简单的计算器语言,可以支持简单的加减法,比如类似js的语法
var id = 123 id = id + 456 var myid = id
执行完这段代码之后,myid的值会变成579,那么如何来实现这个简单的语言呢?我们需要实现一个简单的编译器来完成这个事情。
一个编译器的编译过程一般包括以下几个步骤:
词法分析
词法分析的阶段就是需要识别一个一个的词,比如var id = 123里面就有3个var、id、=、123这些标识符,这些词我们一般称为token,词法分析阶段就是解析token的过程。我们需要一个一个的读取字符,比如读到v的时候,我们需要继续读下一个字符,如果下一个字符是空格或者=,那么v就是变量名。如果下一个字符不是空格或者=,那么需要继续读下一个字符。整体思路就是读取的当前字符可以判断之前的字符串是一个token的话,那么就解析出token,否则就需要继续往下读。我们可以一个一个字符解析然后判断每一种情况来实现词法解析的过程。一般常用的做法是用状态机来实现,每个字符是一种状态。比如一个能识别出var、id、=、+、123的状态机就像下面一样:
用代码实现状态机就可以判断每个token了,词法分析的阶段就完成了
语法分析
语法分析的阶段就是根据词法分析产生的token,形成一个抽象语法树。比如关键字var不能单独使用,后面得跟着一个变量名id,“var id”在一起是有意义的,就是声明一个变量,后面还可以跟=形成赋值,也是可以的。“var var”就是一个错误的语法。所以根据定义好的语法规则,就可以形成语法树。语法树是一棵树状结构,整个代码片段都可以形成一个棵树,还是以最上面的语句为例,可以形成这样一棵树:
程序执行的过程就是执行一条一条的语句,每一条语句对应的是一棵语法树,那么如何处理这棵语法树呢?
(图中ε表示空转换)
但是如果表达式中引用了一个上下文未声明的或者未初始化的变量,类似这种错误需要等到所有语法树生成之后才能判断,这些判断就是语义分析阶段的工作
语义分析
语义分析就是分析程序语法含义的过程。程序语法看上去没问题,但是真实含义有问题,比如有:
类似这种错误需要在所有语法树生成之后,遍历所有语法树的节点才能判断。除了发现语义错误之外,还可以计算一些附加属性,比如表达式的语法树的最大层数,变量的类型,变量的作用域等。除此之外,还可以做一些优化,比如(1+2)+(3+4)表达式对应的语法树,可以把左子树(1+2)计算出来是3,右子树(3+4)计算出来是7,再继续计算出来是10,经过常量计算,语法树就直接求出值了,这样就不需要程序运行期间再求值了
生成中间代码和生成目标代码
生成中间代码和生成目标代码一般指后端语言的编译过程。像Javascript语言,是解释执行的,不需要编译成中间代码或者机器码。比如Java语言,编译之后会生成字节码文件,字节码就是一种中间代码,然后由Java虚拟机解释执行字节码或者编译成机器码执行。假如要实现一门后端编译型语言,生成中间代码的过程可以生成LLVM IR的中间表示形式,然后由LLVM生成机器码。生成中间代码的过程是为了生成跨平台的统一的语言表达形式,就比如Java字节码,每个平台的字节码是一样的,由各个平台的Java虚拟机适配操作系统和底层硬件架构。LLVM IR也是一种跨平台的统一的语言表达形式,再由LLVM框架适配操作系统和底层硬件架构。
为了方便,这里我们生成Java字节码。Java字节码是属于JVM规范的一种(https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.5),字节码的文件格式如下:
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
由类js语法翻译成字节码的思路是:
示例代码如 https://gitee.com/liuanyou/myjs