這裏的虛擬機是指執行一種低級語言字節碼的虛擬機,這個限定可能強了些,比方說,前面說的一個AST解釋器,也可以看做是一種虛擬機,因爲理論上是可以有一個機器解釋AST執行,但這裏我們說的虛擬機是和真機相對的,確切說就是和現在廣泛使用的馮諾依曼結構對應,因此狹義地,只有執行一個個指令的實現才成爲虛擬機VM。具體的實現方式分爲棧虛擬機和寄存器虛擬機,下面主要討論棧虛擬機
在語法分析部分,主要是討論了stmt_list和expr樹,其中stmt_list已經很清楚了,這裏先看expr,對expr的分析,很多資料上會講到逆波蘭表達式,也譯作後綴表達式,這個表達式也可以通過同樣的算法生成,只是輸出的操作不同而已,但是考慮到語義分析(類型檢查,常量靜態計算等),還是expr樹比較方便。後綴表達式和expr樹可以很方便地互相轉換,而後綴表達式中的各元素都直接對應棧虛擬機中表達式計算的字節碼,因此從expr樹就可以直接生成字節碼
例如expr:a+b-c*d/e,根據運算符優先級,最後生成的expr樹大概是這樣(Python類結構表示,假設統一類Expr,類型用運算符屬性op表示,二元運算兩個屬性爲a和b):
Expr(op = "-",
a = Expr(op = "+",
a = Expr(op = "load", name = "a"),
b = Expr(op = "load", name = "b")),
b = Expr(op = "/",
a = Expr(op = "*",
a = Expr(op = "load", name = "c"),
b = Expr(op = "load", name = "d")),
b = Expr(op = "load", name = "e")))
這個expr樹轉字節碼,和其解釋執行的流程一樣,只不過並不是真的執行,而是將執行的步驟記錄下來,中間數據記錄在棧上,比如加法的to_byte_code(僞代碼):
this.a.to_byte_code();
this.b.to_byte_code();
output_byte_code(BYTE_CODE_ADD);
因此上面的樹最終的結果是這麼個樣子:
load a
load b
add
load c
load d
mul
load e
div
sub
其中add,sub,mul,div是加減乘除,運算字節碼沒有參數,是對當前棧頂兩個元素彈出運算,結果寫回棧頂,手工模擬下就能驗證,這個操作序列和AST的執行是一致的,最後的結果存在棧頂
函數調用的expr,區別在於函數的傳入參數可能不同,如果將函數設計爲對象,則做call指令的時候需要帶參數,而且要運行時檢查,比如:
f(a,b,c)
ff(d,e)
結果是:
load f
load a
load b
load c
call 3 //3個參數
load ff
load d
load e
call 2 //2個參數
一般來說都這麼實現,不過如果把函數本身看做一個操作,則可以:
load a
load b
load c
call_f
load d
load e
call_ff
很容易看出,這樣做一個明顯的問題是字節碼必須跟着函數走,因爲函數大都是自定義的,爲減少耦合,幾乎沒有這樣實現語言的,因爲字節碼基本上都是固定的。但並非說這種方式行不通,後面實現語言的時候我會用到類似的技術對代碼進行特化
有了expr的字節碼,stmt的字節碼就很容易寫了,比如expr形式的stmt,算完後用pop指令丟棄棧頂結果即可
賦值形式stmt和上面的區別在於,棧頂結果要存儲到某個地方,這樣就需要根據左值不同採用不同的store指令,如果左值是一個變量,簡單用store就可以,如果左值是一個較複雜的表達式,則一般需要先計算,比如:
a[b + c] = d * e
左值和右值分別計算expr樹,在轉換字節碼的時候,區別在於左值的最後一步運算轉換成相反的存儲指令,假設左值先執行:
load a
load b
load c
add //左值算完,最後一步load_item不寫,棧上留a和b+c的值
load d
load e
mul
store_item //執行時棧裏有三個元素,容器、下標和值
當然,反過來先輸出賦值語句右邊的表達式計算代碼也可以,這個看設計了,會造成語言中表達式的計算順序問題
上面說的字節碼在執行是都是順序解釋執行,除了expr和賦值兩種stmt外,其他大多數語句都和跳轉有關,即字節碼的jmp指令,廣泛用於循環、分支等。函數調用廣義上說也算是一種跳轉,但具體實現上,函數調用有不同的做法,後面再討論
比如while語句的實現:
@START
... //計算condition
pop_jmp_if_false OUT
... //執行內容
jmp START
@OUT
這裏我們用@來表示label,具體實現中應該是一個字節碼的索引位置,就好像彙編語言的label會變成地址一樣,當然跳轉可以做絕對跳轉和相對跳轉,這個比較隨意,視具體情況而定
如果上述循環中出現break和continue語句,則直接jmp至OUT或START即可。另外由於任何循環形式都可以歸約至while循環,道理都是一樣的,就不贅述了
if語句,假設是Python的elif這種並列形式:
... //計算條件1
pop_jmp_if_false IF_2
... //執行內容
jmp OUT
@IF_2
... //計算條件2
pop_jmp_if_false IF_3
... //執行內容
jmp OUT
@IF_3
... //多個if並列的類似代碼
@IF_N
... //計算條件N
pop_jmp_if_false ELSE
... //執行內容
jmp OUT
@ELSE
... //執行內容
@OUT
如果是C和java的if...else形式,則實現更簡單,因爲這裏把AST樹展開了,再深也沒關係