1. 程式人生 > >一個簡單編譯器的實現過程

一個簡單編譯器的實現過程

一、編譯器的實現步驟

1、詞法分析,將原檔案劃分為單獨的單詞或關鍵字。

2、語法分析,利用上下文無關文法分析程式的短語結構。

3、語義分析,根據短語建立抽象語法樹,確定短語含義、變數關聯宣告、檢查表示式型別。

4、翻譯,根據抽象語法樹翻譯成中間表示樹不依賴任何特定的程式語言和目標機器結構。

5、指令選擇,根據目標機器的指令體系,對中間表示樹的節點進行劃分。

6、控制流分析。

7、資料流分析。

8、暫存器分配。

9、程式碼生成。

在上編譯原理課程的時候,我們完成了一個基於miniJava的編譯器和垃圾回收器


程式碼:https://github.com/xuhaoavl/tiger

二、詞法分析器

1、詞法記號,程式語言語法單元的一系列字元,詞法記號一般分為有限的幾種。

例如:ID(字串)、NUM(整數)、REAL(實數)、IF(if)、VOID(void)、COMMA(逗號)。。。。

2、正則表示式,一般用正則表示式匹配字元竄的方式檢查字串型別。

符號(a,v,h,b等),或(a|b)、並(a·b)、空(ε)、重複(克林閉包,所有可能的字串)

有一些可能會產生模糊性,例如if8是字串還是if和8,對此有以下兩種解決方法

a、最長匹配,選擇可以匹配正則表示式的最長輸入字串。

b、優先匹配,選擇最先與之匹配的正則表示式,這時候正則表示式的順序很重要。

3、有限自動機DFA,不存在兩個從統一狀態出發且標記完全相同的邊,不存在空串。

一般使用DFA來實現詞法分析。

4、非確定有限自動機NFA,可能存在多條從一個狀態出發且標記一樣的邊,也可能有空邊。

一般使用正則表示式表示一個語言的詞法,然後將其轉換為NFA(正則表示式轉NFA比較容易),最後再將NFA轉DFA,並用程式碼實現。

三、語法分析

1、上下文無關文法

使用上下文無關文法描述詞法結構。

推導形式有最左推導和最右推導,可以使用一顆分析樹表示推導的過程,每一步推導都對分析樹中的某一個節點進行一步拓展。

二義性文法是指能夠產生兩顆不同的分析樹。,可以通過改寫文法消除二義性。

2、預測分析的過程

遞迴下降,每個遞迴下降分析器對應一個非終結符號的推導。

但是在推導的過程中有可能會產生衝突,即在某一步有多個候選式。

這時候可以通過計算當前非終結符的first集和follow集來解決衝突。

LL(k):從左到右分析,最左推導,k個lookahead符號。

LR(k):從左到右分析,最右推導,k個lookahead符號。

消除左遞迴,根據first集和follow集,左遞迴會導致多重定義入口。

消除左因子,用一個新的非終結符替代。

四、抽象語法

1、在一個遞迴下降分析器中,語義動作是分析函式返回的值。

2、抽象語法樹,其實也就是前面所說的分析樹

在Java中其實也就是一顆物件樹,main函式所在的類也就是樹根,樹上有表示式物件,語句物件等。

如圖所示,在語法分析的過程中,已經構建出了一顆抽象語法樹。

3、抽象語法樹的遍歷

從根步遍歷語法分析樹的時候並不知道每個子節點的型別,可以通過instanceof解決,但是更好的一個解決方法是使用訪問者模式。

意圖:主要將資料結構與資料操作分離。

主要解決:穩定的資料結構和易變的操作耦合問題。


五、語義分析

把變數的定義與使用聯絡到一起,檢查表示式的型別。通過符號表進行。

1、符號表,將識別符號與其型別和位置進行對映。實際中可能含有很多個符號表,每一個類、每一個方法都有一個符號表,第一次遇到該變數的時候會將其加入到符號表中,作用域結束後將其刪除。

符號表主要用於檢查變數的使用和定義、檢查表示式型別。

六、翻譯成中間表示


雖然可以直接翻譯成機器碼,但是這將阻礙可移植性並且無助於進行模組化設計。

中間表示是一種抽象的機器語言,無需過多考慮機器特性就可以對目標機進行表達。但是,它同樣獨立於源語言。編譯器的前端進行詞法分析、語法分析、語義分析並負責生成中間表示,後端負責對中間表示進行優化,同時翻譯成機器語言。

1、使用中間樹表示中間表示

遞迴遍歷前面生成的抽象語法樹將其轉換成中間樹。中間樹與抽象語法樹的區別是抽象語法樹和源語言相關,而中間樹則與源語言無關,與機器也無關,是一種中間表示形式,例如三地址指令、四地址指令等。

中間樹要儘量能夠滿足不同語言的要求,又不能與機器底層有太多關係。

七、指令選擇


中間表示樹的每一個樹節點僅能表示一種操作:從記憶體中讀,從記憶體中寫,加,減等。找到一些恰當的指令去構造一顆中間表示樹是指令選擇階段要完成的工作。

將機器指令表示成中間樹的片段,稱為樹模式。這樣指令選擇的任務就是構造一顆包含最小樹模式集所組成的樹。

八、活性分析


編譯器需要分析中間表示程式來確定哪些臨時變數同時被使用,如果一個變數中儲存的值會被使用,則變數是活躍的。如果兩個臨時變數不是同時使用,則他們可以共用暫存器。通過活性分析可以確定出一個最少使用的暫存器數,最大化暫存器的使用。

控制流圖:每一條語句都是一個執行塊,按照程式執行的順序可以畫出程式的控制流。

通過控制流分析出每個變數的活性,再通過活性畫出干擾圖。

干擾圖可以是一個鄰接矩陣或則一個無向圖,哪些點之間有邊表示那些變數之間不能分配給同一個暫存器。

九、暫存器分配


暫存器分配其實是一個著色問題,使用k(k個暫存器)種顏色給干擾圖著色。

十、垃圾收集


標記清掃、複製、標記整理、分代收集。

十一、資料流分析


資料流分析主要是收集程式執行時可能發生的資訊,並針對該資訊進行一些優化:

1、暫存器分配,同一個暫存器儲存兩個不重疊的臨時變數。

2、消除共用子表示式,如果一個表示式被多次計算,消除其中一個計算。

3、消除死程式碼

4、合併常量,如果一個表示式的運算元是常量,那麼在編譯時執行該計算。

除此之外,還有其他的一些優化策略,如迴圈優化、靜態單賦值、流水線和排程策略。

還有一些利用計算機cache的優化,如指令cache對齊,預取指令迴圈交換,分塊等。