原文地址:http://www.javashuo.com/article/p-wjzwucli-hv.html html
1、前言
以前常常变动学习方向,没有收到很好的学习效果,浪费了很多时间。最近痛定思痛,把方向定为JVM和编译原理,此次真的不改了。本文是学习该方向的阶段性总结。前端
以前写过几个解释器,但还没写过编译器。恰好看到知乎Belleve给出的一幅学习路线图,因而决定实现一个lisp方言的编译器。java
之因此选择JVM而不是X86做为目标平台,一是JVM日常用的多一些,能够互相印证、互相补充;二是文档和社区资源丰富友好,开发体验较好。git
项目地址:https://github.com/rigophypheriveri/slispgithub
截止最新的commit 77f126d4
,实现的功能有:后端
- 定义变量
- 支持字符串、整数和布尔类型
- 打印以上三种预置类型的值
- 四则运算
- 条件判断
2、编译和运行方法
来一段具体的Slisp程序:网络
(define a (+ 1 2 3 4)) (println a) (define b (+ a a)) (println b) (define a (+ b b)) (println a) (println (+ (+ 1 1) (- 6 4) (* 2 2) (/ 4 2))) (println "Hello Slisp!") (define c "Hello world!") (println c) (println true) (println false) (define d true) (println d) (if true (println true) (println false)) (if (== 1 1) (println "1 == 1") (println "1 != 1"))
以上程序出自本项目/Slisp/Hello.slisp。app
想要运行必须先打包编译器:post
./gradlew clean build
获得了build/libs/slisp-0.1.0.jar
,以后在命令行编译源代码:学习
java -jar build/libs/slisp-0.1.0.jar Slisp/Hello.slisp
便可生成Hello.class
文件,java Hello
运行该文件,输出为:
10 20 40 10 Hello Slisp! Hello world! true false true true 1 == 1
3、编译器组成部分
这个编译器由三部分组成,一是前端部分,二是构建抽象语法树,三是递归降低生成字节码。
前端部分使用了Antlr来构建。Antlr是一个流行的parser generator,能够根据给定的文法,生成相应的parser。由于Slisp自己采用了lisp系的语法,并不复杂,因此很容易写出文法供Antlr使用。
构建抽象语法树使用了visitor模式。因为Antlr自己返回的结果已是一棵树,因此这部分的工做是,根据每一个节点不一样的形态建立相应的类和实例。
这里有一些实现上的细节能够优化,好比针对四则运算,能够将这些运算所有用一个类来表示,只更改其中的一个字段以示区别。还有一点是,若是打算只使用一个visitor,那么每一个节点类都须要继承同一个接口或父类。
还有,实现了一点简单的类型推导。传统的lisp方言大可能是动态语言,不过Slisp是静态的,并且能够在定义变量时推导出变量的类型,不须要开发者手动声明变量的类型。(define a 123)
、(define b "Hello")
和(define c true)
能够由字面值推导出类型,而(define d (+ 1 (- 2 3))
也能够推导出表达式(+ 1 (- 2 3))
的类型并以此肯定d
的类型。
生成字节码部分采用了递归降低来生成。好比对(+ (+ 1 1) (- 6 4) (* 2 2) (/ 4 2))
,生成了:
44: bipush 1 46: bipush 1 48: iadd 49: bipush 6 51: bipush 4 53: isub 54: iadd 55: bipush 2 57: bipush 2 59: imul 60: iadd 61: bipush 4 63: bipush 2 65: idiv 66: iadd
这段代码是Hello.class文件中的一部分,使用OpenJDK中的javap反汇编器生成。
(+ 1 1)
对应4四、46和48,先将两个1压入栈中,而后相加,将以前的两我的从栈中弹出,而后将结果压入栈顶,继续执行(- 6 4)
。
这里须要注意的是,并非说执行完这四个运算(+ 1 1) (- 6 4) (* 2 2) (/ 4 2)
,而后再计算它们的和。而是在计算完(+ 1 1)
和(- 6 4)
以后(结果为2和2),当即计算了(+ 2 2)
(获得4),而后计算(* 2 2)
(获得4),再计算(+ 4 4)
,以此类推。过程以下所示:
(+ (+ 1 1) (- 6 4) (* 2 2) (/ 4 2)) (+ 2 (- 6 4) (* 2 2) (/ 4 2)) (+ 2 2 (* 2 2) (/ 4 2)) (+ 4 (* 2 2) (/ 4 2)) (+ 4 4 (/ 4 2)) (+ 8 (/ 4 2)) (+ 8 2) (10)
为了契合这样的字节码运算方式,后端在建立抽象语法树的时候须要注意“左结合与右结合”的问题。这里采用了右结合的方式,大体结构以下所示:
(+ (/ 4 2) (+ (* 2 2) (+ (- 6 4) (+ 1 1))))
这样从底层开始生成字节码,每生成一层,就向上传递,继续生成上层节点的字节码。
实际开发中使用了ASM库来辅助生成字节码,只须要手动拼接好相似于bipush 1
这样的文本传给ASM中合适的类和方法,最后调用generateBytecode
这样的方法便可。
虽然ASM库很方便,但想要生成符合语义的字节码,开发者仍须要阅读JVM规范。JVM规范中定义了各字节码的名称与语义,对照着网络上的众多示例仍是很容易理解的。
4、字节码简介
bipush
是指将一个类型为byte
扩充为int
,而后压到栈上。
iadd
是将栈最上面的两个int
弹出,而后计算它们的和,将结果压入栈顶。imul
、isub
和idiv
都相似于iadd
,不一样之处在于将运算符变为了*
、-
和/
。
istore
将int
保存在局部变量中。
iload
从局部变量中取出保存在其中的值。
astore
是将对一个Ojbect
的引用保存在局部变量中。
alocal
是将保存在局部变量中的引用压入栈顶。
ifeq
是将栈顶的值与0
进行比较,若是相等,进入true branch,不然进行false branch。该指令还会指定一个数字做为false branch入口的地址。
if_icmpne
是比较栈上的两个类型为int
的值,若是不相等,进入true branch,不然进入false branch。
值得注意的是,诸如if
这样的指令并非单个存在,它们更多的像是一个家庭,好比比较两个int
会有许多类似的指令,从JVM规范中抄录一段:
• if_icmpeq succeeds if and only if value1 = value2 • if_icmpne succeeds if and only if value1 ≠ value2 • if_icmplt succeeds if and only if value1 < value2 • if_icmple succeeds if and only if value1 ≤ value2 • if_icmpgt succeeds if and only if value1 > value2 • if_icmpge succeeds if and only if value1 ≥ value2
能够看到if_icmpne
只是用来比较两个数相等时的状况,还有其它指令用于比较不等、大于、小于、相等时的状况。像这样类似而略有区别的指令,JVM规范大多将它们的文档合并在一块儿,并起名为if_icmp<cond>
,这里的cond
表明每一个指令独特的部分。