實現一門語言的中間代碼生成器(4小時)
實驗目的
通過本次實驗,加深對中間代碼生成的理解,學會編制中間代碼生成器。
實驗任務
用C、JAVA或其他語言編寫一門語言的中間代碼生成器,所選實現語言應與之前語言保持一致。
實驗內容
- 實現中間代碼生成器,可以將任一源語言(源語言儘量與前期實驗中的源語言保持一致)轉化成三地址碼(或其他中間表示形式)。
- 準備2~3個測試用例,測試你的程序,並解釋生成的中間代碼。
源代碼下載和說明
鏈接:https://pan.baidu.com/s/1Ogf4447oPMrxHVJE8_hwmg
密碼:fc7f
運行方法:同實驗一TINY編譯器(這其實就是實驗一的工程)
說明:實驗七中間代碼生成器直接使用了TINY語言。在2018-2019年秋季學期,湖南大學編譯原理課首次將本實驗變爲必做(之前是選做,但由於難度太大,基本沒有學長學姐寫),故本實驗採用已有的代碼。
實驗知識點講解和函數源代碼分析
1、中間代碼生成的任務
中間代碼生成屬於編譯器前端結構的最後一部分,首先,編譯器前端讀入代碼,對代碼進行詞法分析,構建出符號序列,再將符號序列傳入語法分析,構造出語法樹;接下來的語義分析則是一個靜態檢查的過程,它判斷上下文的各個結點是否符合語法規則,並報錯,生成符號表,而接下來的中間代碼生成則也是對於語法樹進行操作,傳入一棵語法樹,從根結點,根據該節點的詞法屬性,分析詞法結點之間的邏輯,翻譯成合適的中間表示。
在TINY語言中,需要將NO_CODE標記位設置爲真,這樣就能夠輸出中間代碼的生成結果。
2、實現TINY語言的中間代碼生成器
本次實驗要求實現一箇中間代碼生成器,則我們採用的方法是增量編程,在之前所構造好的TINY前期組件基礎之上,構建中間代碼的生成部分。
2.1 TINY語言的中間代碼生成結果——TM CODE
TINY語言可以被翻譯爲一個適用於TM虛擬機環境的代碼表示:TM CODE。TM CODE其實是類似於彙編指令的程序語言,但是何其不同的地方在於,TM CODE可以在爲TINY語言所構建的虛擬機中運行,它模擬了彙編代碼中的一些特性,比如說寄存器操作,它在CODE.H頭文件中定義了幾個寄存器的值(地址),使得這樣的一種基於寄存器操作的類彙編語言能夠執行。
T機的模擬程序直接從一個文件中讀取彙編代碼並執行它,因此應避免將由彙編語言翻譯爲機器代碼的過程複雜化。但是,這個模擬程序並非是一個真正的彙編程序,它沒有符號地址或標號。因此,TINY編譯器必須仍然計算跳轉的絕對地址。此外爲了避免與外部的輸入/輸出例程連接的複雜性,TM機有內部整型的I/O設備;在模擬時,它們都對標準設備讀寫。
下圖展示了TM CODE的詳細定義:
我們注意到裝入操作中有3個地址模式並且是由不同的指令給出的:LDC是“裝入常量”,LD是“由存儲器裝入”,而LDA是“裝入地址”。另外,該地址通常必須給成“寄存器+偏差”值。例如“10(1)”(上面代碼的第2條指令),它代表在將偏差10加到寄存器1的內容中計算該地址。(因爲在前面的指令中,0已被裝入到寄存器1中,這實際是指絕對位置10)。我們還看到算術指令MUL和ADD可以是“三元”指令且只有寄存器操作數,其中可以單獨確定結果的目標寄存器。
2.2 MAIN函數調用入口及文件預處理
如圖代碼展示了MAIN函數的文件預處理和中間代碼生成的調用入口:
第一步是文件的預處理。
strcspn函數的作用是,在pgm字符串中查找到第一個“.”,並返回它之前的所有字符作爲子串,這樣做的目的在於,我們傳入給編譯器的文件是一個文件,我們中間代碼的輸出結果也需要保存在一個文件中,輸出文件在這裏這樣做,是爲了保持和輸入文件同名。
strncpy函數的作用是拷貝字符串,這裏用於將strcspn提取的文件名存入字符串供輸出文件使用。
接下來程序使用提取的文件名創建了一個.TM文件,作爲中間代碼的輸出,並打開它,賦予其“w”寫的權限,並執行中間代碼生成的後續操作。
【這裏大家注意一下,運行完程序之後,文件夾裏面會出現一個.TM結尾的文件,用記事本打開,就是其生成的TM CODE,也就是運行結果】
第二步就是中間代碼生成,它調用了codeGen函數,傳入了語法樹根結點以及需要寫入的文件,進行後續操作,下面代碼所展示的是該函數:
這個代碼主要是調用了emitRM,emitComment函數插入了TM虛擬機的初始化指令,其中這些函數傳入的都是mp、ac等定義好的寄存器,這個在code.H中有詳細定義:
TM虛擬機不是一開始就能運行中間代碼的,需要一些初始化的條件,在插入完這些指令之後,就會到達一個正式的cGen的代碼生成過程,待到cGen函數執行完畢,繼續需要插入一條停機指令HALT代表代碼執行完畢。
2.3 代碼生成的具體過程
下圖展示的是cGen函數的代碼:
可以看到,傳入的語法樹在一邊遍歷的同時,檢查結點的類型,TINY語言中分爲兩種語句結點,一種是帶有關鍵字的保留語句,一種是表達式,比如賦值或者是算式(總之,不帶有保留關鍵字),這兩種情況分開考慮。
1、getStmt——分析含有保留關鍵字語句的函數
該函數的作用主要是處理TINY語言中所包含的五個關鍵字——if,repeat,assign,read,write。
這裏以if作爲一個例子來分析說明這個函數需要做的工作:if結點包括三個子結點,if本身的判斷表達式、then、以及else(通常,else可以被省略)。我們對於每個if的子結點遞歸分析(因爲if的子結點可能也會是一個表達式,比如if的條件判斷,這樣的話就需要對它進行遞歸分析)。
用savedLoc變量記錄遞歸的返回位置,待分析完這一條路徑之後,就可以找到函數在哪裏被調用了,在調用的過程中,由於各個節點都需要遞歸地訪問,因此這裏在處理下一個節點的時候,使用了emitSkip這個函數,用於跳過並保存當前點的位置,以便於函數最後的返回工作。【這個地方,也叫作回填】
其他的處理也是類似的,比如在repeat語句裏面,repeat包含的是兩個結點,一個是repeat它本身,第二個是與之對應的until條件,同if一樣,分爲兩塊進行分別的一個遞歸處理。
其他三個關鍵字分別是assign,read,write。這三個的處理比較簡單,因爲他們的語句結構決定了他們只有一個子結點,因此,直接處理子結點就可以了。
如何處理子結點呢?
如圖所示,我們獲取了結點的類型,也能夠獲取結點的邏輯關係,此時,只要調用剛剛提到的函數emit,就可以將這條指令寫到輸出文件中,使得最後的TM虛擬機能夠執行完成。
2、getExp——處理表達式
處理表達式結點的邏輯比較簡單,如果表達式的結點類型是ID或者數字,那麼直接使用LD命令加載它即可。
如果結點是一個運算符,那麼據我們所知,運算符是由子結點構成的,它的子結點就是運算的兩個數字,或者是ID字符。我們需要使用LD命令將子結點裏面的具體數字或字符讀取出來,再根據運算符的類型構造相應的TM code命令。
下圖展示的是小於和等於的命令TM code:
運行結果
輸入數據:(文件名:SAMPLE.TNY)
{ Sample program
in TINY language -
computes factorial
}
read x; { input an integer }
if 0 < x then { don't compute if x <= 0 }
fact := 1;
repeat
fact := fact * x;
x := x - 1
until x = 0;
write fact { output factorial of x }
end
輸出:(文件名:SAMPLE.TM)
* Standard prelude:
0: LD 6,0(0) load maxaddress from location 0
1: ST 0,0(0) clear location 0
* End of standard prelude.
2: IN 0,0,0 read integer value
3: ST 0,0(5) read: store value
* -> if
* -> Op
* -> Const
4: LDC 0,0(0) load const
* <- Const
5: ST 0,0(6) op: push left
* -> Id
6: LD 0,0(5) load id value
* <- Id
7: LD 1,0(6) op: load left
8: SUB 0,1,0 op <
9: JLT 0,2(7) br if true
10: LDC 0,0(0) false case
11: LDA 7,1(7) unconditional jmp
12: LDC 0,1(0) true case
* <- Op
* if: jump to else belongs here
* -> assign
* -> Const
14: LDC 0,1(0) load const
* <- Const
15: ST 0,1(5) assign: store value
* <- assign
* -> repeat
* repeat: jump after body comes back here
* -> assign
* -> Op
* -> Id
16: LD 0,1(5) load id value
* <- Id
17: ST 0,0(6) op: push left
* -> Id
18: LD 0,0(5) load id value
* <- Id
19: LD 1,0(6) op: load left
20: MUL 0,1,0 op *
* <- Op
21: ST 0,1(5) assign: store value
* <- assign
* -> assign
* -> Op
* -> Id
22: LD 0,0(5) load id value
* <- Id
23: ST 0,0(6) op: push left
* -> Const
24: LDC 0,1(0) load const