表示式(expression)在程式語言中代表乙個可以返回值的語法單位,比如常量表示式,變數表示式,函式呼叫表示式,算術、關係和邏輯表示式等等。對於函式式程式語言來說,幾乎所有的語句都是表示式,可以被估值。而對於命令式語言,一般會將語句分成表示式和陳述語句(statement)。表示式可以被估值,而普通的陳述語句用來執行命令。根據具體的語法,這兩種型別不一定會有明確的界限。比如在c中,a = b既是乙個用來賦值的陳述語句,又是乙個表示式,而作為表示式的結果是最終的a值。所以,像c = a = b這樣的語句是成立的,意思是將a = b作為表示式,並將值賦給c。
而在lua中,表示式的描述要明確的多。a = b屬於乙個賦值statement,而不屬於表示式,所以c = a = b會產生語法錯誤。唯一即可以當作expression又可以當作statement使用的就是call。call本身會呼叫函式,返回函式的返回值,而作為statement時,返回值被忽略。
根據lua5.2完整的bnf,我們可以看到lua中僅有以下地方需要使用表示式:
變數賦值,等號左邊必須是乙個變數表示式,右邊是乙個任意表示式
區域性變數的初始化,等號右邊是任意表示式
if statement的條件表示式和迴圈的條件表示式
在需要表示式的地方,通過呼叫expr函式,並傳入乙個expdesc結構體物件,對表示式進行解析。表示式的解析是乙個遞迴下降的過程。下降分析將高層的表示式分解成底層表示式或表示式的組合,而遞迴則發生在expr函式的遞迴呼叫上,也就是說在解析過程中還會用表示式本身來描述高層表示式。當解析到bnf的終結符時,會返回上一層處理,然後再一層層的處理後返回。expr函式最終會填充傳入的expdesc結構體,作為最高層的根表示式,交給更高層的語義,也就是上面需要表示式的地方進行處理。
lua關於遞迴下降分析的每個函式的注釋中都有代表這個函式的bnf正規化,我們可以很容易的瀏覽這些**,不需要過多的解釋。真正需要理解的是表示式與指令生成相關的部分,這也是整個lua編譯系統裡面比較晦澀的地方。我們可以首先通過乙個簡單的例子,在巨集觀上了解一下語法分析和指令生成的全過程。
對於下面的chunk
c = a.b + 1
我們最終可以生成如下指令
main (5 instructions at 0x80048eb8)
0+ params, 2 slots, 1 upvalue, 0 locals, 4 constants, 0 functions
1 [1] gettabup 0 0 -2 ; _env "a"
2 [1] gettable 0 0 -3 ; "b"
3 [1] add 0 0 -4 ; - 1
4 [1] settabup 0 -1 0 ; _env "c"
5 [1] return 0 1
constants (4) for 0x80048eb8:
1 "c"
2 "a"
3 "b"
4 1
locals (0) for 0x80048eb8:
upvalues (1) for 0x80048eb8:
0 _env 1 0
整個的遞迴下降語法分析過程可以用下圖表示。
由於我們目前需要講解的是表示式,這裡為了講解方便,這裡省略了一些過程。接下來我們對這些步驟逐一進行解說。
exprstat函式呼叫suffixedexp函式,對賦值語句的左邊的字尾表示式進行分析。
這裡沒有展開suffixedexp函式,我們目前只需要知道它會返回乙個vindexed表示式。
exprstat呼叫expr函式,對賦值右面的表示式進行分析。如上所述,expr函式是解析表示式的總入口,他接受乙個expdesc結構體,開始分析。
expr呼叫subexpr
subexpr函式首先呼叫******exp,來分析「+」號左邊的表示式。
******exp呼叫suffixedexp函式,將這個表示式當成字尾表示式開始分析。
suffixedexp函式首先呼叫primaryexp函式,分析主表示式,也就是a。
primaryexp呼叫singlevar函式,將a當作乙個變數進行分析。
singlevar沒有找到名字為"a"的區域性變數或upvalue,將"a"當作全域性變數處理,也就是將"a"變成「_env.a"來處理。這裡已經到了遞迴下降分析的最低端,最終建立乙個vindexed的表示式給上層,table為upvalue "_env",key為常量」a「。
繼續返回vindexed表示式給上層。
suffixedexp將這個vindexed表示式傳給fieldsel,對字尾進行分析。
fieldsel首先根據這個vindexed表示式的table和key生成指令1,這個指令的目標暫存器為臨時分配的暫存器0。然後以暫存器0為table,」b「為key,生成乙個新的vindexed表示式返回給上層。
繼續返回vindexed表示式給上層。
繼續返回vindexed表示式給上層。
subexp呼叫subexp本身,開始對」+「號右邊的表示式進行分析。
subexp呼叫******exp,分析這個」1「。
******exp為這個"1"生成乙個vknum表示式,返回給上層。
繼續返回vknum表示式給上層。
subexp首先根據+號左邊的vindexed表示式的table和key生成指令2,這個指令的目標暫存器為臨時分配的暫存器0。然後生成指令3的加法運算,運算元為暫存器0和vnum表示式對應的常量id。指令3的目標暫存器還不能確定,所以建立乙個vrelocable表示式返回給上層。
這時整個表示式已經解析完畢,返回vrelocable表示式給上層,等待進一步的處理。
將vrelocable表示式對應的指令3的目標暫存器回填成臨時分配的暫存器0,然後將暫存器0的內容賦值給左邊的vindexed表示式,也就是生成指令4。
通過上面的分析過程我們可以看到,lua整體的語法分析過程就是對語法樹的一次性的先續遍歷的過程。對於表示式的分析,首先要分析子表示式,並為其生成指令來獲取表示式的值,存入臨時暫存器,然後父表示式再使用子表示式的分析結果和臨時暫存器作為引數,來生成獲取值的指令。所有在過程中使用的子表示式的expdesc結構體物件全部在函式的呼叫棧上分配,待分析完成返回後,就被丟棄掉了。由於lua本身的指令是基於暫存器的,一條指令所能完成的任務相對比較複雜,所以有些情況下在子表示式分析過程中不能完全獲得所需要的資訊。這是就需要將表示式分析所得的資訊返回給上一層父表示式,也就是子表示式的使用者,由上一層做最終的指令生成。或者先生成子表示式指令,然後在上一層分析中進行指令的回填修改。我們在上例中就可以清晰地看到這種情況。
在《虛擬機器指令》中我們提到過,lua使用的是register based vm,所以相對於stack based vm來說,整個編譯和指令生成過程要更複雜。暫存器在lua中的第乙個用處就是儲存區域性變數的值,所有區域性變數在編譯後,都不再使用名稱,而是暫存器id進行訪問。而另乙個用處就是儲存表示式估值過程中的臨時值。當對乙個表示式進行估值時,可能先要對其子表示式進行估值,將估值結果儲存到乙個臨時的暫存器,然後使用這個結果再進行下一步的估值計算。暫存器為乙個id從0開始的陣列。在編譯過程中,lua使用funcstate中的freereg變數記錄當前空閒暫存器的起始id。在開始編譯乙個funcstate時,freereg被設定成0,表示所有暫存器都可以被分配。當遇到乙個區域性變數或者臨時值時,就分配出乙個id為當前freereg的暫存器,然後將freereg++。區域性變數會在語法域內一直占用這個暫存器,而臨時值會在使用完其值後立即被釋放,也就是freereg--。由於臨時值會在表示式估值完成後全部釋放掉,所以區域性變數被分配的暫存器肯定是從0開始並且是連續的,中間不會被臨時值占用。
總的來說,區域性變數與臨時值沒有什麼本質區別,都是用來存放函式計算過程中表示式的值得,唯一區別就在於臨時值不占用暫存器,而區域性變數會一直占用暫存器,並且可以被程式訪問。
上面的例子中,12,19和21步中都需要臨時暫存器的分配。我們看到在需要臨時暫存器的指令生成之後,臨時暫存器就被被釋放掉了,所以每次分配時都會將暫存器0分配給臨時值使用,而不會一直占用暫存器0。
在後面的文章中,我將會按照分類對表示式進行詳細的講解。
lua5 2模組註冊
lua 5.2不鼓勵使用lual register把模組註冊到全域性域,而是使用lual newlib。我們首先看下lual newlib,它是乙個巨集,如下 define lual newlib l,l lual newlibtable l,l lual setfuncs l,l,0 lual n...
lua 5 2的 luaL newlib 的用法
對於lua 5.2,lua 5.2是2011年發布的。國內使用5.1的居多。lual register 和lual openlibs。這些功能大多數都廢棄了 lual register這個註冊c 的庫函式,功能被廢棄了。新的api lual newlib 網上的資料不多。下面給出乙個使用 lual ...
lua5 2 帶你理解 ENV和 G
5.1之前,全域性變數儲存在 g這個table中,這樣的操作 a 1 相當於 g a 1 但在5.2之後,引入了 env叫做環境,與 g全域性變數表產生了一些混淆,需要從原理上做乙個理解。在5.2中,操作a 1 相當於 env a 1 這是乙個最基礎的認知改變,其次要格外注意 env不是全域性變數,...