原文地址: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
表明每一個指令獨特的部分。