天天看點

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

原文:http://blog.csdn.net/yuanlin2008/article/details/8534583

常量表達式

常量表達式在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代表對一個表進行索引的變量表達式,比如a.b或者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。如下圖所示:

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

函數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,等待回填生成指令的目标寄存器。

關系表達式

關系表達式(>,>=,<,<=,==,~=)在遵循傳統意義上應該對兩個待比較的表達式的關系進行評估,然後将評估結果的boolean值作為表達式的估值,提供給高層使用。而Lua并沒有這樣處理,而是将傳統的語義轉化成一個條件分支,直接代表着“這個表達式為true或false時應該做些什麼”。這樣的轉換完全是為了執行效率。在實際使用中,關系表達式大多數被用在條件跳轉判斷中,而不是指派。這樣實作可以直接連接配接後面的分支,省了很多不必要的步驟。

關系表達式在codecomp函數中建立。首先,将需要比較的兩個子表達式取出目标寄存器或者常量id。然後通過調用condjump函數,為表達式生成指令,并将表達式設定成VJMP類型。condjump函數會生成一個測試指令和一個跳轉指令OP_JMP。測試指令包括OP_EQ,OP_LT,OP_LE。這些測試指令會比較兩個表達式的值。如果滿足測試條件,就繼續執行;否則,就跳過下一條指令執行。測試指令與後面的OP_JMP指令配合到一起,就形成了一個條件跳轉。這樣的表示方法也是Lua中條件跳轉的唯一表示方法。條件跳轉形成了兩個執行分支:

  • 當測試條件滿足,就跳轉到一個現在還未知的位置,也就是true分支。
  • 當測試條件不滿足,就繼續運作OP_JMP後面的指令,也就是false分支。
探索Lua5.2内部實作:編譯系統(4) 表達式分類

這個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作為作為整個表達式解析傳回給高層的表達式。

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

對于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值。

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

是以,表達式的值最終會由很多的分支導緻很多的值。将表達式的值指派給寄存器的操作,在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。

繼續閱讀