編譯器

大家好,好久沒看到喵了,是不是想我了。

這是本喵看到的一篇好文章,所以忍不住想要拿過來。

知乎上有一種說法是「編譯器、圖形學、操作系統(tǒng)是程序員的三大浪漫」。

先不管這個說法是對是錯,我們假設(shè)一個程序員在國內(nèi)互聯(lián)網(wǎng)公司寫代碼,業(yè)余時間不看相關(guān)書籍。那么三年之后,他的這些知識會比在校時損耗多少?

很顯然,損耗的比例肯定非常高,畢竟國內(nèi)互聯(lián)網(wǎng)公司日常開發(fā)工作中,程序員基本很少接觸這三塊知識。大部分程序員工作幾年后對編譯原理相關(guān)的概念只能生理上起反應(yīng),腦海里很難再串聯(lián)起相關(guān)概念了。

編譯原理的概念有讓人看到就頭痛的特質(zhì),學校里要死記硬背,考試過了巴不得趕緊全忘掉,相信不少同學現(xiàn)在看到下面概念還會覺得蛋疼:

非確定性有限自動機/確定性有限自動機

四元式序列

上下文無關(guān)文法/BNF

終結(jié)符/非終結(jié)符

LL(1)/LR(1)

特設(shè)語法制導轉(zhuǎn)換

局部優(yōu)化

其實本喵在學習這門課的時候也很煩腦,畢竟這是號稱最難學的一門學科之一。本喵是在大四的時候?qū)W的這門課,經(jīng)過半個學期死命的看書,也還是懵懵懂懂,不怎么明白。但好在考試的時候過了。

什么是編譯器

廣義的編譯器可以指任意把一種語言代碼轉(zhuǎn)為另一種語言代碼的程序

做編譯器實際上都需要做什么?

編譯器是一整套工具鏈,從前端的詞法分析、語法分析,到中間表示生成、檢查、分析、優(yōu)化,再到代碼生成。

如果是編譯器從業(yè)者,大部分時間在做中間這塊;如果是業(yè)余愛好者,大部分時間在做前端和代碼生成。

先確定源語言:

這是一門看起來像lisp的四則運算語言,四個雙目運算符分別是「add」「sub」「mul」「div」。

多項四則運算可以這樣寫:

(mul(sub5(add12))4

再來確定目標語言:

同樣是一門四則運算語言,但是看起來可讀性更強,對應(yīng)的四個雙目運算符分別是「+」「-」「*」「/」。

上面源語言的例子編譯完后應(yīng)該是這樣:

((5 -(1 +2))* 4)

最后確定我們寫編譯器要用的語言:

喵選擇Haskell,有兩個原因,一是寫Haskell有大名鼎鼎的ParseC,寫Parser非常方便;二是Haskell的代數(shù)數(shù)據(jù)類型的定義本身就是AST。

ParseC的全稱是Parser組合子。Parser,抽象理解就是一個輸入為字符串輸出為類型T的值的函數(shù)。ParseC庫實現(xiàn)了大量基礎(chǔ)Parser和Parser組合子,Parser組合子可以將庫自帶的基礎(chǔ)Parser和用戶定義的Parser隨意組合成新的更強大的Parser。

舉個例子,你實現(xiàn)了一個Parser,功能是根據(jù)輸入文本返回解析到的標識符名稱。ParseC庫實現(xiàn)了一個名叫many的parser組合子,跟你自己的Parser組合起來就產(chǎn)生了一個新的Parser:可以根據(jù)輸入文本返回解析到的標識符名稱list。

為什么要用ParseC呢?因為用ParseC定義Parser具有PEG(解析表達式文法,原理不細講,不影響接下來學習)的所有好處,同時還不用再學習語言之外的知識(比如用flex和bison前要先學習這兩者自己的「DSL」)。

當然,其他語言也有類似的庫,比如c++有boost::spirit,Java/C#/F#/JS有Haskell的ParseC的工業(yè)級實現(xiàn)。這些語言跟Haskell的區(qū)別無非在于要寫一些額外的邏輯把Parser的解析結(jié)果轉(zhuǎn)成AST。

如果沒有接觸過Haskell的話也沒關(guān)系,接下來的示例代碼都非常declarative,非常self-descriptive,請放心食用。

接下來就開始寫代碼了,首先我們要定義AST的結(jié)構(gòu),目的是為了能用這個結(jié)構(gòu)描述一切源語言表達式。

簡單分析一下源語言,我們可以直接得出表達式這個概念的遞歸定義:一個表達式要么是一個字面值,要么是一個雙目運算符和兩個表達式的求值結(jié)果。

然后是字面值這個概念的遞歸定義:一個字面值要么是一個整型值,要么是一個浮點型值。

在Haskell里面這兩個定義寫成下面這樣:

跟前面的文字定義對應(yīng)一下:

表達式Exp,要么是一個字面值表達式ConstExp,由一個Val組成;要么是一個雙目運算表達式BinOpExp,由一個操作符和兩個Exp組成。

值Val,要么是一個整型值IntVal,由一個Integer組成;要么是一個浮點型值FloatVal,由一個Float組成。

接下來開始寫Parser。流程是先為AST中的每個節(jié)點類型寫一個parser,然后再把這些parser組合起來形成能parse出整棵AST的parser。

我們先給自己定個小目標,比如先實現(xiàn)一個int_parser。

p_int是能從文本中Parse出Integer的Parser定義。而p_int_val改造了p_int,定義了能從文本中Parse出IntVal的Parser。

然后我們把int和float的parser組合起來成為一個val_parser。

listplus可以簡單理解為并,在具體實現(xiàn)上會做回溯。

同理,我們先分別實現(xiàn)ConstExp的parser和BinOpExp的parser,再把兩者組合為exp_parser。

到目前為止,我們的parser部分就完工了。

對Haskell有興趣的同學,可以安裝下ghci,是haskell的REPL,然后加載剛才寫好的Parser.hs,在命令行里試一下

可以看到輸出結(jié)果。稍微排版下,輸出結(jié)果變成了我們熟悉的樹形結(jié)構(gòu),Op為「mul」的BinOpExp就是樹的根節(jié)點。整個輸出就是一棵AST。

有了這棵AST,我們就可以開始做后續(xù)的代碼生成了。

CodeGenerator的主體是把Exp轉(zhuǎn)換成目標語言代碼的函數(shù):

利用模式匹配這個語言特性實現(xiàn)多態(tài)既容易又優(yōu)雅。

最后再套個殼,比如讀源文件,寫目標文件,整個編譯器就大功告成了

好了,到了和大家說再見的時候了。如果有興趣可以去:http://mp.weixin.qq.com/s?__biz=MzIwNDU2MTI4NQ==&mid=2247483679&idx=1&sn=8df4b40386fb6182051f4926ab043636#rd這個網(wǎng)址看看。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容