Friday, March 15, 2013

探索Lua5.2內部實現:編譯系統(4) 表達式分類


常量表達式

常量表達式在Lua用來表示"nil",“true”,“false”,字符串和數字的值。在BNF中常量表達式屬於終結符,也就是語法解析的最底端,在simpleexp函數中被解析出來,並創建對應類型的expdesc對象。VNIL,VTRUE和VFALSE這三個類型本身就對應3個固定的值,沒有什麼額外的數據。VKNUM類型代表數字常量,需要在nval中存放從詞法分析中得到的lua_Number。VK類型用來表示一個通常意義上的常量表達式,使用info來存儲他所代表的常量值在常量表中的id。字符串常量就被直接創建成VK類型,然後將其對應的字符串值保存到常量表中,並將id保存到info中。
由於常量表達式的值是一個常量,所以本身不需要生成任何用於估值計算的指令,完全為高層語義的指令生成提供服務。當高層語義要將常量裝入一個寄存器時,比如local a="foo",會調用discharge2reg函數,生成OP_LOADK指令。Lua中的很多指令都可以直接使用常量作為操作數,比如算數指令。當高層語義要將常量當作其他指令的參數時,會調用luaK_exp2RK函數,返回這個常量對應的id。
其實Lua本身使用VK一種類型就可以表示所有的常量表達式,而其他類型常量表達式完全是為了優化才使用的。VNIL和VTRUE,VFALSE類型分別都有對應OP_LOADNIL和OP_LOADBOOL指令將常量值裝入寄存器,所以不需要將其放到常量表中。我們在《虛擬機指令》中講過這些指令的特殊用法。而VNUM的作用是支持算數運算的常量優化(constfolding),如果被優化掉了,也就不需要在常量表中出現。VNUM會在discharge2reg的時候,也就是真正需要使用其常量值得時候,才被放到常量表中。當VNIL,VTRUE,VFALSE和VNUM需要被用在其他指令的參數時,luaK_exp2RK將其全部轉化為VM類型,並在常量表中創建對應的常量值。VNIL,VTRUE和VFALSE還會在邏輯表達式中被用於優化。

變量表達式

VLOCAL代表局部變量表達式,在info中保存局部變量對應的寄存器id。
VUPVAL代表upvalue變量表達式,在info中保存upvalue的id。
VINDEXED代表對一個表進行索引的變量表達式,比如ab或者a[1],使用ind結構體保存數據。idx保存用來索引的key的id,他可能是一個寄存器id或者常量id;t保存被索引表的id,他可能是一個寄存器id或者upvalue id;vt表示t的類型是寄存器id(VLOCAL)還是upvalue id(VUPVAL)。
singlevar函數用來解析一個變量表達式。singlevar調用函數singlevaraux,來查找變量名對應的表達式類型。singlevaraux首先在當前FuncState的局部變量中查找。如果找到,就創建一個VLOCAL表達式。否則,就遞歸向上查找外圍函數的upvalue,並最終返回一個VUPVAL表達式。這個查找的過程也是創建upvalue的過程。當在外圍函數中找到對應變量名的局部變量後,會在這個外圍函數的所有內嵌函數中創建對應的upvalue。如下圖所示:
函數func(N)內嵌於func(N-1)。當func3使用了變量a,並在func0中找到了局部變量a時,會在func1~3中創建a對應的upvalue。func1中的upvalue是對上一級中的局部變量的直接引用,所以isstack為1,idx代表局部變量的寄存器id。其他的都是對上一級upvalue的引用,所以isstack為0,idx代表上一級upvalue的id。
如果singlevaraux最終沒有能找到符合變量名的局部變量或者upvalue,singlevar函數就將這個變量名當作全局變量進行處理。首先singlevar會再次調用singlevaraux,查找名稱為“_ENV”的upvalue。這個upvalue已經在最外層的mainfunc中創建了,所以一定能找到。然後將_ENV對應的upvalue變量表達式當作table,用變量名當作key,通過luaK_indexed函數創建一個VINDEXED表達式。這個操作等於將varname轉化為_ENV.varname進行訪問。
變量表達式的估值計算是從變量中獲取值。luaK_dischargevars函數為變量表達式生成估值計算的指令。對於VLOCAL類型,值就存在於局部變量對應的寄存器中,不需要生成任何獲取指令,也不需要分配寄存器來存儲臨時值。VLOCAL被轉化為VNONRELOC類型,代表已經為這個表達式生成了指令,並且也分配了寄存器保存這個值。對於VUPVAL類型,需要產生指令OP_GETUPVAL來獲取其值。而對於VINDEXED類型,根據vt的不同,需要產生OP_GETTABLE或者OP_GETTABUP指令來獲取其值。VUPVAL和VINDEXED都被轉化為VRELOCABLE類型,表示獲取指令已經生成,但是指令的目標寄存器(A)還沒有確定,等待回填。回填後,VRELOCABLE類型會轉化成VNONRELOC類型。
變量表達式除了用來獲取變量值,還有另外一個用途,就是在賦值語句中當作賦值的目標,也就是將其他表達式的值存儲到這個變量表達式中。這個工作是由luaK_storevar函數完成的。luaK_storevar根據被賦值的變量表達式的不同類型,生成不同的賦值指令。對於VLOCAL,不需要額外的指令,只需要將賦值表達式的目標寄存器回填成局部變量對應的寄存器就可以了。對於VUPVAL,需要生成OP_SETUPVAL指令。而對於VINDEXED,則需要生成OP_SETTABLE或者OP_SETTABUP指令。

多返回值表達式

VCALL表達式對應著一個函數調用的OP_CALL指令。VVARARG表達式對應"...",也就是OP_VARARG指令。他們都可以有多個返回值。在需要單值的上下文中,通過調用luaK_setoneret函數,將表達式設置成單返回值。VCALL的返回值位置是固定的,就是用來存放被調用closure的寄存器,所以被轉化為VNONRELOC類型。VVARARG的目標寄存器待定,被轉化為VRELOCABLE類型。在需要多個返回值的上下文中,通過調用luaK_setreturns函數,回填指令中的返回值數量。

算數表達式

所有關於操作符的解析都在subexpr函數中進行,這里處理一元和二元操作符以及優先級關係。對於一元操作符,會調用luaK_prefix生成代碼。對於二元操作符,會首先調用luaK_infix處理第一個前面的表達式,然後分析出後面的表達式,再調用luaK_posfix對這兩個表達式進行處理。
算術表達式最終是在codearith函數中創建的。首先,codearith會調用constfolding函數,嘗試優化。如果兩個被操作表達式都是數字常量,就直接計算出結果賦給第一個常量表達式。如果不能被優化,codearith函數直接為算術表達式生成對應的算數指令,並且將表達式類型設置成VRELOCABLE,等待回填生成指令的目標寄存器。

關係表達式

關係表達式(>,>=,<,<=,==,~=)在遵循傳統意義上應該對兩個待比較的表達式的關係進行評估,然後將評估結果的boo​​lean值作為表達式的估值,提供給高層使用。而Lua並沒有這樣處理,而是將傳統的語義轉化成一個​​條件分支,直接代表著“這個表達式為true或false時應該做些什麼”。這樣的轉換完全是為了執行效率。在實際使用中,關係表達式大多數被用在條件跳轉判斷中,而不是賦值。這樣實現可以直接連接後面的分支,省了很多不必要的步驟。
關係表達式在codecomp函數中創建。首先,將需要比較的兩個子表達式取出目標寄存器或者常量id。然後通過調用condjump函數,為表達式生成指令,並將表達式設置成VJMP類型。condjump函數會生成一個測試指令和一個跳轉指令OP_JMP。測試指令包括OP_EQ,OP_LT,OP_LE。這些測試指令會比較兩個表達式的值。如果滿足測試條件,就繼續執行;否則,就跳過下一條指令執行。測試指令與後面的OP_JMP指令配合到一起,就形成了一個條件跳轉。這樣的表示方法也是Lua中條件跳轉的唯一表示方法。條件跳轉形成了兩個執行分支:
  • 當測試條件滿足,就跳轉到一個現在還未知的位置,也就是true分支。
  • 當測試條件不滿足,就繼續運行OP_JMP後面的指令,也就是false分支。
這個OP_JMP指令的位置會被保存到表達式的info中,等待後繼的回填處理。

邏輯表達式

邏輯表達式(and,or)的處理是最複雜的。表達式e1 and e2的語義是:如果e1為true,整個表達式的值為e2;否則整個表達式的值為e1。表達式e1 or e2的語義與and相反:如果e1為false,整個表達式的值為e1;否則整個表達式的值為e2。所以,邏輯表達式其實是兩個待評估表達式的二選一。
首先要先說明一下expdesc結構體中的t和f變量。這兩個變量實際上是兩個OP_JMP指令的鍊錶(關於跳轉鍊錶,在這裡已經講過),我們稱之為patch list。由於關係表達式和邏輯表達式的特殊處理方式,這兩個patch list代表本表達式被評估為true或者false時的跳出指令列表。通過將一個地址回填給patch list,就將對這個表達式的評估直接引導到對應的執行分支。任何類型的表達式都可能帶有patch list。如果有patch list,說明這個表達式本身或者子表達式使用了關係或者邏輯表達式。
patch list中的元素是在luaK_goiftrue和luaK_goiffalse函數中被添加到patch list中的。luaK_goiftrue函數調用jumponcond生成一個OP_TESTSET和一個OP_JMP指令。與關係表達式的處理類似,這兩個指令形成了兩個執行分支:向下執行的true分支和跳出的false分支。這個跳出的OP_JMP指令會被連接到表達式的f中,代表表達式為false時執行的跳出,並等待回填。如果表達式有t存在,調用luaK_patchtohere,將所有true跳出回填到下面繼續執行的代碼。這代表了除了本表達式評估為true會繼續執行外,所有該表達式被評估為true的跳出也應該跳轉到此處繼續執行。如果表達式是一個關係表達式,也就是VJMP類型,其本身就是一個邏輯跳轉,直接將其info中指向的OP_JMP指令連接到f中,而不需要為其生成OP_TESTSET指令了。luaK_goiffalse與luaK_goifture相反,當表達式為true時跳出。
對於關係表達式e1 and e2,首先在luaK_infix中為e1調用luaK_goiftrue,生成對e1的測試跳轉指令,這就代表如果e1為true,繼續執行;否則跳出。此時e1中的f中已經包含了e1的false跳出。然後解析e2,並在luaK_posfix中,將e1的f串接到e2的f上,並將e2作為作為整個表達式解析返回給高層的表達式。
對於or的處理,與and類似,這裡不再累述。
OP_TESTSET與其他測試指令類似,唯一的特殊點在於OP_TESTSET在執行跳轉時會將被評估的表達式的值賦給一個未知的寄存器。這個操作時專門針對邏輯表達式的語義設計的。如果將邏輯表達式e1 and e2的值賦值給一個變量,那麼這個變量的值並不是true或者false,而是e1或者e2。所以,在跳轉時,說明此跳轉已經取了e1的值,要先做一個對未知寄存器的賦值,然後等待回填這個寄存器。

表達式的用途

根據上面描述,表達式在Lua中本質上有兩種用途。
首先是被當作跳轉條件使用。在if,while等結構中,都需要一個表達式來代表跳轉條件。在當作條狀條件處理時,Lua會使用上面講過的luaK_goiftrue或者luaK_goiffalse,來評估一個表達式。此時表達式本身的值已經沒有意義,只需要將t或者f回填成分支起始指令未知就搞定了。如果t或f中有OP_TESTSET指令,會被替換成OP_TEST指令。
其次是將表達式的值保存到寄存器中,比如給變量賦值。由於上面關係和邏輯表達式的特殊處理,表達式的值已經不僅僅是對本身的估值,還要考慮到patch list所對應的值。我們可以將patch list中的條件跳轉分為兩種。使用OP_TESTSET作為條件指令的,也就是關係表達式產生的條件跳轉,我們稱之為賦值條件跳轉TS。上面講過,如果走到這個分支,表達式的最終值就是OP_TESTSET所設置的值。其他所有不使用OP_TESTSET的測試指令,也就是關係表達式所產生的跳轉,我們稱之為非賦值條件跳轉T。分條件跳轉最後應該產生true或者false值。
所以,表達式的值最終會由很多的分支導致很多的值。將表達式的值賦值給寄存器的操作,在exp2reg函數中完成。要將這些值賦給一個寄存器,首先調用discharge2reg,將自己的值存入指定寄存器,然後處理patch list。對於非賦值條件跳轉,我們需要為true和false值各補上一個OP_LOADBOOL指令作為賦值操作,並將t中的所有T回填到true,將f中所有的T回填到false。而對於賦值條件跳轉,不僅要將跳轉回填到賦值的最後,並且需要回填所有OP_TESTSET指令的目標寄存器,也就是將這些值賦值給指定寄存器。patchlistaux (FuncState *fs, int list, int vtarget, int reg, int dtarget)函數就是回填patch list用的。此函數會遍歷patchlist上的每個條件跳轉,如果是OP_TESTSET,就回填寄存器reg,並回填跳轉地址vtarget,否則,回填跳轉地址dtarget。

No comments: