goto
在Lua5.2中,goto和label是新加入的statement,用來執行非條件跳轉。這兩個statement分別在lparser.c中的gotostat和labelstat函數中被解析。上一篇中講過,在全局數據Dyndata中,保存著一個goto列表和一個label列表,goto和label使用一個相同的數據結構Labeldesc表示。
- /* description of pending goto statements and label statements */
- typedef struct Labeldesc {
- TString *name; /* label identifier */
- int pc; /* position in code */
- int line; /* line where it appeared */
- lu_byte nactvar; /* local level where it appears in current block */
- } Labeldesc;
name用來表示label的名稱,用來相互查找。如果是label,pc表示這個label對應的當前函數指令集合的位置,也就是待跳轉的指令位置;如果是goto,則代表為這個goto生成的OP_JMP指令的位置。nactvar代表解析此goto或者label時,函數有多少個有效的局部變量,用來在跳轉時決定需要關閉哪些upvalue。
gotostat接受一個已經為之生成好了的OP_JMP指令的位置,首先通過newlabelentry為這個goto在ls->dyd->gt中生成一個Labeldesc,用來表示未處理的goto,然後調用findlabel嘗試處理這個goto。findlabel會在當前的block中查找已經定義了的label。如果找到,就調用closegoto,將這個goto對應的OP_JMP指令的跳轉位置設置成label的pc,並且還要決定是否需要在OP_JMP指令中關閉一些局部變量對應的upvalue;如果沒有找到,這個goto對應的OP_JMP指令的跳轉位置就是一個NO_JUMP,表示未決位置,等待後面再處理。
與gotostat的處理類似,labelstat在處理label時,也會首先查找已經定義但未決的goto。如果找到了goto,也要修改其OP_JMP指令的跳轉位置。整個goto和label語法分析的代碼比較直接,並不難理解。而這裡比較晦澀的是Lua對OP_JMP指令的處理方法。對於OP_JMP的處理還會在後面的關係和邏輯運算,以及條件跳轉中使用。所以,理解Lua對OP_JMP指令的處理是理解其他編譯部分的基礎。
OP_JMP
前面講過,由於Lua是一編編譯,所以在真正生成指令時,很多東西是沒法決定的。比如OP_JMP指令,要跳轉的位置可能還沒有定義,所以不能知道具體的跳轉位置。Lua會將這些指令先生成到函數proto的指令列表中,然後記錄下他們在指令列表的位置。當可以確定時,再通過這個位置找到生成好的指令,對其進行修改。這就是指令回填。
對於一個OP_JMP指令,有兩個指令參數需要回填:待關閉的upvalue起始id A和跳轉偏移量sBx。當通過luaK_jump函數生成一個OP_JMP指令時,這個指令的A會被初始化成0,而sBx被初始化成NO_JUMP,並返回一個int代表這個OP_JMP指令的位置。我們保存這個返回值就可以找到這個指令,對其進行回填。
在語法分析過程中(比如後面要講到的關係和邏輯運算),有可能會生成一系列的OP_JMP指令,他們的跳轉目標是一致的,從而形成一個跳轉指令集合。Lua使用了鍊錶的方式保存這種指令集合。這個鍊錶每個節點就是OP_JMP指令本身,使用sBx來指向鍊錶的下一個OP_JMP指令。如果sBx為NO_JUMP,表示鍊錶的尾節點。所以我們只需要一個指向頭節點的位置,就可以遍歷整個OP_JMP指令鍊錶。lcode.c中對於OP_JMP指令的處理函數,都是基於一個跳轉鍊錶的,只不過在一些情況下,鍊錶中只有一個節點而已。如果需要回填一個具體的跳轉地址,就會遍歷鍊錶,將每個節點的跳轉地址都修正到目標位置。
回填跳轉地址可以分為兩類處理:
一種是已經生成的指令位置,Lua會立即將這些跳轉指令修改成目標位置。
另一種是當前位置,也就是接下來要生成的指令的位置。在處理當前位置時,Lua並沒有立即修改,而是將待修改的跳轉指令串接到當前FuncState中的jpc鍊錶上。當使用luaK_code生成下一條指令時,首先會調用dischargejpc函數,將jpc鍊錶上的所有跳轉指令修改到這個位置。這等於是延遲回填。之所以這樣處理,其實是為了跳轉的優化。我們回頭看一下luaK_jump函數,它在生成OP_JMP指令時,會將當前jpc串接到新生成的jpc上,並且將jpc清空。這個處理實際的意思是,當生成一個OP_JMP指令時,如果有其他的OP_JMP指令需要跳轉到此處,其實就等於間接跳轉到新生成的跳轉指令的目標位置。所以這些跳轉指令不用再跳轉到此處,而是直接跳轉到新生成的OP_JMP的目標位置,將兩步跳轉合併成一步。這些指令與新生成跳轉指令的目標是一致的,所以可以合併成一個跳轉指令集合,等待後面一起回填。
我們接下來在來回顧一下與跳轉相關的指令生成api。
luaK_jump用來生成一個新的跳轉指令。luaK_concat函數用來將兩個鍊錶連接起來合成一個鍊錶。luaK_patchlist用來回填一個鍊錶的跳轉位置。luaK_patchtohere用來將一個鍊錶準備回填到當前位置。luaK_patchclose用來回填一個鍊錶中的A,也就是需要關閉的upvalue id,具體可以查看goto的處理。
以上是Lua處理跳轉的相關內容。跳轉的處理還與邏輯和關係表達式密切相關,我們會在後面表達式部分再進行詳細講解。
No comments:
Post a Comment