1. 程式人生 > >手把手教你做一個 C 語言編譯器(8):表示式

手把手教你做一個 C 語言編譯器(8):表示式

這是整個編譯器的最後一部分,解析表示式。什麼是表示式?表示式是將各種語言要素的一個組合,用來求值。例如:函式呼叫、變數賦值、運算子運算等等。

表示式的解析難點有二:一是運算子的優先順序問題,二是如何將表示式編譯成目的碼。我們就來逐一說明。

本系列:

運算子的優先順序

運算子的優先順序決定了表示式的運算順序,如在普通的四則運算中,乘法 * 優先順序高於加法 +,這就意味著表示式 2 + 3 * 4 的實際執行順序是 2 + (3 * 4) 而不是 (2 + 3) * 4

C 語言定義了各種表示式的優先順序,可以參考 C 語言運算子優先順序

傳統的程式設計書籍會用“逆波蘭式”實現四則運算來講解優先順序問題。實際上,優先順序關心的就是哪個運算子先計算,哪個運算子後計算(畢竟叫做“優先順序”嘛)。而這就意味著我們需要決定先為哪個運算子生成目的碼(彙編),因為彙編程式碼是順序排列的,我們必須先計算優先順序高的運算子。

那麼如何確定運算子的優先順序呢?答曰:棧(遞迴呼叫的實質也是棧的處理)。

舉一個例子:2 + 3 - 4 * 5,它的運算順序是這樣的:

  1. 將 2 入棧
  2. 遇到運算子 +,入棧,此時我們期待的是+的另一個引數
  3. 遇到數字 3,原則上我們需要立即計算 2+3的值,但我們不確定數字 3 是否屬於優先順序更高的運算子,所以先將它入棧。
  4. 遇到運算子 -,它的優先順序和 + 相同,此時判斷引數 3 屬於這前的 +。將運算子 + 出棧,並將之前的 2 和 3 出棧,計算 2+3 的結果,得到 5 入棧。同時將運算子 - 入棧。
  5. 遇到數字4,同樣不能確定是否能立即計算,入棧
  6. 遇到運算子 * 優先順序大於 -,入棧
  7. 遇到數字5,依舊不能確定是否立即計算,入棧
  8. 表示式結束,運算子出棧,為 *,將引數出棧,計算 4*5 得到結果 20 入棧。
  9. 運算子出棧,為 -,將引數出棧,計算 5-20,得到 -15 入棧。
  10. 此時運算子棧為空,因此得到結果 -15
1234567891011121314151617181920212223 // after step 1, 2||+------+|3|||+------++------+|2||+|+------++------+// after step 4||||+------++------+|5||-|+------++------+// after step 7||+------+|5|+------++------+|4||*|+------++------+|5||-|+------++------+

綜上,在計算一個運算子‘x’之前,必須先檢視它的右方,找出並計算所有優先順序大於‘x’的運算子,之後再計算運算子‘x’。

最後注意的是優先通常只與多元運算子相關,單元運算子往往沒有這個問題(因為只有一個引數)。也可以認為“優先順序”的實質就是兩個運算子在搶引數。

一元運算子

上節中說到了運算子的優先順序,也提到了優先順序一般只與多元運算子有關,這也意味著一元運算子的優先順序總是高於多元運算子。因為我們需要先對它們進行解析。

當然,這部分也將同時解析引數本身(如變數、數字、字串等等)。

關於表示式的解析,與語法分析相關的部分就是上文所說的優先順序問題了,而剩下的較難較煩的部分是與目的碼的生成有關的。因此對於需要講解的運算子,我們主要從它的目的碼入手。

常量

首先是數字,用 IMM 指令將它載入到 AX 中即可:

C
12345678 if(token==Num){match(Num);// emit code*++text=IMM;*++text=token_val;expr_type=INT;}

接著是字串常量。它比較特殊的一點是 C 語言的字串常量支援如下風格:

C
123 char*p;p="first line""second line";

即跨行的字串拼接,它相當於:

C
12 char*p;p="first linesecond line";

所以解析的時候要注意這一點:

C
12345678910111213141516 elseif(token=='"'){// emit code*++text=IMM;*++text=token_val;match('"');// store the rest stringswhile(token=='"'){match('"');}// append the end of string character '', all the data are default// to 0, so just move data one position forward.data=(char*)(((int)data+sizeof(int))&(-sizeof(int)));expr_type=PTR;}

sizeof

sizeof 是一個一元運算子,我們需要知道後面引數的型別,型別的解析在前面的文章中我們已經很熟悉了。

C
12345678910111213141516171819202122232425262728 elseif(token==Sizeof){// sizeof is actually an unary operator// now only `sizeof(int)`, `sizeof(char)` and `sizeof(*...)` are// supported.match(Sizeof);match('(');expr_type=INT;if(token==Int){match(Int);}elseif(token==Char){match(Char);expr_type=CHAR;}while(token==Mul){match(Mul);expr_type=expr_type+PTR;}match(')');// emit code*++text=IMM;*++text=(expr_type==CHAR)?sizeof(char):sizeof(int);expr_type=INT;}

注意的是隻支援 sizeof(int)sizeof(char) 及 sizeof(pointer type...)。並且它的結果是int 型。

變數與函式呼叫

由於取變數的值與函式的呼叫都是以 Id 標記開頭的,因此將它們放在一起處理。

C
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182 elseif(token==Id){// there are several type when occurs to Id// but this is unit, so it can only be// 1. function call// 2. Enum variable// 3. global/local variablematch(Id);id=current_id;if(token=='('){// function callmatch('(');// ①// pass in argumentstmp=0;// number of argumentswhile(token!=')'){expression(Assign);*++text=PUSH;tmp++;if(token==','){match(',');}}match(')');// ②// emit codeif(id[Class]==Sys){// system functions*++text=id[Value];}elseif(id[Class]==Fun){// function call*++text=CALL;*++text=id[Value];}else{printf("%d: bad function call\n",line);exit(-1);}// ③// clean the stack for argumentsif(tmp>0){*++text=ADJ;*++text=tmp;}expr_type=id[Type];}elseif(id[Class]==Num){// ④// enum variable*++text=IMM;*++text=id[Value];expr_type=INT;}else{// ⑤// variableif(id[Class]==Loc){*++text=LEA;*++text=index_of_bp-id[Value];}elseif(id[Class]==Glo){*++text=IMM;*++text=id[Value];}else{printf("%d: undefined variable\n",line);exit(-1);}//⑥// emit code, default behaviour is to load the value of the// ad