這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
原文在此。
————翻譯分隔線————
編譯器(10)-編譯到 C
第一部分:介紹
第二部分:編譯、轉譯和解釋
第三部分:編譯器設計概覽
第四部分:語言設計概述
第五部分:Calc 1 語言規格說明書
第六部分:標識符
第七部分:掃描
第八部分:抽象文法樹
第九部分:解析
終於到最後一個步驟了!
我們的語言規格說明書如此簡單,其實可以跳過 C 直接輸出彙編。我有兩個不這麼做的原因。首先,移植性。在這個指引中,我無須編寫任何特定架構的 C 代碼。C 已經被移植到各種不同的系統中去了,因此可以讓 C 編譯器為我們做這個工作。
其次,對於許多程式員來說,彙編比起 C 來說要陌生得多。即使你從未使用 C 編寫任何東西,它也比彙編要容易理解得多。
編譯
這看起來會有點熟悉。我們將遍曆 AST 同時產生 C 代碼並進行計算。
編譯器產生並輸出到一個與原檔案相同名字,副檔名為“.c”的檔案中。CompileFile 建立輸出檔案,解析原始碼並啟動輸出處理。
與解析器相同,AST 的第一個元素將是 File 對象。不過 File 對象本身並未包含除了根運算式之外的任何有用的東西,它提供了輸出內容的機制。
這一部分並未有任何令人興奮的內容。只是一系列的模板。我們為程式提供了一個入口,一個退出狀態,以及一個必須的 printf 語句和 C 標準輸入/輸出標頭檔。
最佳化
在本系列的開頭,我說過不會討論過多關於最佳化的內容,事實如此。從另外一個角度來說,我們的語言適合一種最佳化:預計算。
我們可以檢視樹中每個元素的對應的代碼,但是結果如何呢?它會如此複雜,並且毫無益處。換個角度,為什麼不在進行這個步驟的時候就進行計算,讓最終的輸出簡單並且迅速呢?
如果根運算式是一個數,那麼就將這個數傳遞給 printf。如果它是個二值運算式,會有其他一些樂趣。
第一站是通用函數 compNode,用它來判斷我們有什麼對象:BasicLit 還是 BinaryExpr。
如果是基本文法元素,也就是我們所熟知的整數,只需要將其轉換到實際的整數,並返回結果。
二值運算式也是同樣簡單的。運算式列表中的第一個元素永遠都是起點。運算數的順序對於像除法、減法這樣的運算來說非常重要。運算式列表的第一個元素作為起點儲存在一個臨時變數裡(換句話說,這有些類似使用彙編中的 eax 或 rax 寄存器)。
然後基於運算子,針對每個運算數計算出結果並且儲存回相同的變數。完成後返回結果。這一過程會遞迴進行,知道所有內容都完成。
最終環節
在編譯器完成了這些工作之後,還有一些需要完成的內容。首先,需要建立一個命令來讀取 Calc 1 的原始碼,並且調用編譯器的庫。其次,C 編譯器(例如 gcc 或 clang)會針對編譯器命令的輸出內容進行工作。最後,鑒於 C 編譯器的運行方式,你可能還需要對 C 編譯器輸出的目標檔案執行連結器。
所有這些是由一個叫做 calcc 的程式處理的,一個可選的 Calc 編譯器。這是我超級聰明的命名技巧的另外一個驗證。
這個程式沒有什麼特別的。它開啟一個輸入的檔案,驗證其具有 .calc 的副檔名,然後對其調用 comp.CompileFile。然後使用來自 Go 標準庫的 exec.Command 來執行 C 編譯器和連結器。
還有若干命令列參數來對 C 編譯器進行控制。
結束語
我希望這對於那些想要瞭解並學習一些關於編譯器知識的人有所協助。這是一個相當龐大的話題,並且幾乎無法駕馭。
對於某些感覺這個系列可能過於簡單的人來說,我只能抱歉了。我希望你們能有空和我一起,繼續向前推進,到達 Calc 2。
我跳過了大量的內容,因此我希望能在接下來的系列中招手解決這些缺陷。將在 Calc 2 中包含的內容有:
- 符號表
- 範圍
- 函數定義
- 變數定義
- 對比和分支
- 變數賦值
- 類型
- 記憶體棧
我將會基於 Calc 1 的代碼來實現 Calc 2,因此在這裡你所學到的任何東西,我想,都會適用於下一個系列。
如果 Calc 3 最終完成,我真心希望如此,肯定會糾纏於彙編。如果這樣的話,可能需要單獨包含一些關於彙編本身的文章,或者作為 Calc 3 的獨立的導引系列。還有其他一些想法:對象、方法、多次賦值、多檔案和庫。
感謝閱讀!
祝你好運,再見!