寫給前端的零基礎(chǔ)編譯原理科普
本文是 @神說要有光 對編譯小白 ssh 的一次答疑解惑,很適合零基礎(chǔ)的新手第一次了解編譯原理的概念,故分享出來。
近些年,編譯原理在前端領(lǐng)域的應(yīng)用越來越多,大家比較熟悉的有工程化領(lǐng)域各種轉(zhuǎn)譯器:babel、typescript、eslint、terser、prettier、postcss、posthtml、taro、vue template compiler 等。
由此來看,在前端生涯的路上,難以避免的要遇到編譯原理這座大山。以這篇文章為起點,開始你的編譯原理之旅吧~
昊昊是一個前端工程師,最近涉及到工程化領(lǐng)域,想了解一些編譯的知識。恰好我比他研究的早一些,所以把我了解的東西給他介紹了一遍,于是就有了下面的對話。
介紹一下自己,大家可以叫我神光,17 年畢業(yè),目前就職于阿里高德的跨端架構(gòu)組,做前端工程化相關(guān)的建設(shè),包括 builder 和 ide 等。
大家也可以在知乎-神說要有光[1]關(guān)注我。
前端為什么要學(xué)編譯原理?
昊昊:編譯原理這么深入的東西,前端有必要學(xué)嗎?
我:學(xué)習(xí)編譯原理對前端來說,大概想了這么幾個好處,當(dāng)然實際上肯定遠遠不止這些:
babel、typescript、postcss、posthtml、prettier、eslint、stylelint、taro、vue template compiler,jsx 等的轉(zhuǎn)譯器的深入掌握 JS 解釋器比如 v8 的原理的掌握,甚至自己寫一個解釋器 wasm 是其他語言編譯到 wasm 字節(jié)碼的,掌握這個也需要懂編譯 dart 既可以 JIT 又可以 AOT,要理解它的原理也要懂編譯 需要自己實現(xiàn) dsl 的時候,也要懂編譯原理 ide 的插件,language server provider 的實現(xiàn)需要懂 parser,懂編譯原理
什么是編譯啊?
昊昊:最近想了解一些編譯的東西,光哥,編譯到底是什么啊?
我:編譯啊就是一種轉(zhuǎn)換技術(shù),從一門編程語言到另一門編程語言,從高級語言轉(zhuǎn)換成低級語言,或者從高級語言到高級語言,這樣的轉(zhuǎn)換技術(shù)。
昊昊:什么是高級語言,什么是低級語言啊?
我:低級語言是與機器有關(guān)的,涉及到寄存器、cpu 指令等,特別“低”,描述具體在機器上的執(zhí)行過程,比如機器語言、匯編語言、字節(jié)碼等。高級語言則沒有這些具體執(zhí)行的東西,主要用來表達邏輯,而且提供了條件、循環(huán)、函數(shù)、面向?qū)ο蟮忍匦詠斫M織邏輯,然后通過編譯來把這些描述好的高級語言邏輯自動轉(zhuǎn)換為低級語言的指令,這樣既能夠方便的表達邏輯,又不影響具體執(zhí)行。說不影響執(zhí)行也不太對,因為如果直接寫匯編,能寫出效率最高的代碼,但是如果是高級語言通過編譯來自動轉(zhuǎn)換為低級語言,那么就難以保證生成代碼的執(zhí)行效率了,需要各種編譯優(yōu)化,這是編譯領(lǐng)域的難點。
其實想想,我們把腦中的想法,把制訂好的方案轉(zhuǎn)換為高級語言代碼,這個過程是不是也是轉(zhuǎn)換,可不可以自動化呢,這就涉及到 ai 了。現(xiàn)在有理解需求文檔生成代碼的智能化技術(shù)的研究方向。
昊昊:那具體是怎么轉(zhuǎn)換的呢?
我:要轉(zhuǎn)換首先得了解轉(zhuǎn)換的雙方,要轉(zhuǎn)換的是什么,轉(zhuǎn)換到什么。比如高級語言到高級語言,要轉(zhuǎn)換的是字符串,按照一定的格式組織的,這些格式分別叫做詞法、語法,整體叫做文法,那要轉(zhuǎn)換的目標(biāo)呢,目標(biāo)如果也是高級語言那么要了解目標(biāo)語言的格式,如果目標(biāo)是低級語言,比如匯編,那要了解每條指令時干啥的。然后就要進行語義等價的轉(zhuǎn)換,注意這個“語義等價”,通過一門語言解釋另一門語言,不能丟失或者添加一些語義,一定要前后一致才可以。
知道了轉(zhuǎn)換的雙方都是什么,就可以進行轉(zhuǎn)換了,首先得讓計算機理解要轉(zhuǎn)換的東西,什么叫“計算機理解“呢?就是把我們規(guī)定的那些詞法、語法格式告訴計算機,怎么告訴呢?就是數(shù)據(jù)結(jié)構(gòu),要按照一定的數(shù)據(jù)結(jié)構(gòu)把源碼字符串解析后的結(jié)果組織起來,計算機就能處理了。這個過程叫做 parse,要先分詞,再構(gòu)造成語法樹。
其實不只是編譯領(lǐng)域需要“理解”,很有很多別的領(lǐng)域也要“理解”:
全文搜索引擎也要先把搜索的字符串通過分詞器分詞,然后根據(jù)這些詞去用同樣分詞器分詞并做好索引的數(shù)據(jù)庫中去查,對詞的匹配結(jié)果進行打分排序,這樣就是全文搜索。
人工智能領(lǐng)域要處理的是自然語言,他也要按照詞法、語法、句法等等去“理解”,變成一定的數(shù)據(jù)結(jié)構(gòu)之后,計算機才懂才能處理,然后就是各種處理算法的介入了。
分詞是按照狀態(tài)機來分的,有限狀態(tài)機 DFA,這個是干啥的,為啥分詞需要它,我知道你肯定有疑問。因為詞法描述的是最小的單詞的格式,比如標(biāo)識符不能以數(shù)字開頭,然后后面加字母數(shù)字下劃線等,這種,還有關(guān)鍵字 if、while、continue 等,這些不能再細分了,再細分沒意義啊。分詞就是把字符串變成一個個的最小單元的不能再拆的單詞,也叫 token,因為不同的單詞格式不同,總不能寫 if else 來處理不同的格式吧。其實還真可以,wenyan 就是 if else,吐槽一下。但是當(dāng)有 100 中單詞的格式要處理,全部寫成 if else,我的天,那代碼還能看么。所以要把每個單詞的處理過程當(dāng)成一種狀態(tài),處理到不同的單詞格式就跳到不同的狀態(tài),跳轉(zhuǎn)的方式自然是根據(jù)當(dāng)前處理的字符來的,處理一個字符串從開始狀態(tài)流轉(zhuǎn)到不同的狀態(tài)來處理,這樣就是狀態(tài)自動機,每個 token 識別完了就可以拋出來,最終產(chǎn)出的就是一個 token 數(shù)組。沒錯,就像你說的吃豆子一樣。其實狀態(tài)也不只一級的,你想想比如一個 html 標(biāo)簽的開始標(biāo)簽,可以作為一個狀態(tài)來處理,但這個狀態(tài)內(nèi)部又要處理屬性、開始標(biāo)簽等,這就是二級狀態(tài),屬性又可以再細分幾個狀態(tài)來處理,這是三級狀態(tài),這是分治的思想,一層層的處理。
分詞之后我們拿到了一個個的單詞,之后要把這些單詞進行組裝,生成 ast,為啥一定要 ast 呢?我知道你肯定想問。其實高級語言的代碼都是嵌套的,你看低級語言比如匯編,就是一條條指令,線性的結(jié)構(gòu),但是高級語言呢,有函數(shù)、if、else、while 等各種塊,塊之間又可以嵌套。所以自然要組織成一棵樹形數(shù)據(jù)結(jié)構(gòu)讓計算機理解,就是 Abtract Syntaxt Tree,語法樹、而且是抽象的,也就是忽略了一些沒有含義的分隔符,比如 html 的<、>、</等字符,js 的{ }() [] ;就是細節(jié),不需要關(guān)心,注釋也會忽略掉,注釋只是分詞會分出來,但是不放到 ast 里面。
怎么組裝呢,還是嵌套的組裝,那是不是要遞歸組裝,是的,你想的沒錯需要遞歸,不只是這里的 ast 組裝需要遞歸,后面的處理也很多遞歸,除非到了線性的代碼的階段,就像匯編那樣,你遞歸啥,沒嵌套的結(jié)構(gòu)可以遞歸了。詞法我們剛才分析了,就是一個個的字符串格式,語法呢,是組裝格式,是單詞之間的組合方式。這也是為啥我們剛剛要先分詞了,要是直接從字符串來組裝 ast,那么處理的是字符串級別,而從 token 開始是單詞級別, 這就像讓你用積木造個城堡,但是積木也要你自己用泥巴造,那你怎么造呢,可以先把一個個積木造好,然后再去組裝成城堡,也可以邊造積木邊組裝。不過小汽車的話你可以邊制作積木,邊組裝,城堡級別的邊做積木邊組裝你能理清要造啥積木么,就很難,所以還是要看情況。用這兩種方式來做 parser 的都有,簡單的可以邊詞法分析,分析出熱乎乎的單詞然后馬上組裝到 ast 中, 比如 html、css 這種,但是像 js、c++這種,如果不先分詞,直接從字符串開始造 ast,我只能說太生猛了。
說了半天積木和組裝,那么怎么組裝呢,從左到右的處理 token,遇到一個 token 怎么知道他是啥語法呢,這就像怎么知道一塊積木是屬于那個部件的。也有兩種思路,一種是你先確定這個積木是屬于那個部件,然后找到那個部件的圖紙,按照圖紙來組裝,另一種是你先組裝,組裝完了再看看這個是啥部件。這就是兩種方式,先根據(jù)一兩個積木確定是哪個部件,再按照圖紙組裝這個部件,這種是 ll 的方式,先組裝,組裝完了看看是啥部件,這種是 lr 的方式。ll 的方式要確定組裝的是啥 ast 節(jié)點要往下看幾個,根據(jù)要看幾個來確定組裝的是什么就分別是 LL(1),LL(2)等算法。ll 也就是遞歸下降,這是最簡單的組裝方式,當(dāng)然有人覺得 lr 的方式也挺簡單。ll 有個問題還必須得用 lr 解決,那就是遞歸下降遇到了左邊一直往下遞歸不到頭的情況,要消除左遞歸,也就是你按照圖紙來組裝搞不定的時候,就先組裝再看看組裝出來的是啥吧。這其實和人生挺像的,一種方式是往下看兩步然后決定當(dāng)前怎么走,另一種方式是先走,走到哪步再說。其實我就屬于第二種,沒啥計劃性。
經(jīng)過詞法、語法分析之后就產(chǎn)生了 ast。用一棵樹形的數(shù)據(jù)結(jié)構(gòu)來描述源代碼,從這里開始就是計算機可以理解的了,后續(xù)可以解釋執(zhí)行、可以編譯轉(zhuǎn)換。不管是解釋還是編譯都需要先 parse,也就是要先讓計算機理解他是什么,然后再決定怎么處理。
后面把樹形的 ast 轉(zhuǎn)換為另一個 ast,然后再打印成目標(biāo)代碼的字符串,這是轉(zhuǎn)譯器,把 ast 解釋執(zhí)行或者專成線性的中間代碼再解釋執(zhí)行,這是解釋器,把 ast 轉(zhuǎn)成線性中間代碼,然后生成匯編代碼,之后做匯編和鏈接,生成機器碼,這是編譯器。
編譯器是咋處理 AST 的?
昊昊:光哥,那編譯器是怎么處理 ast 的啊?
我:有了 ast 之后,計算機就能理解高級語言代碼了,但是編譯器要產(chǎn)生低級語言,比如匯編代碼,直接從 ast 開始距離比較遠。因為一個是嵌套的、樹形的,一個是線性的、順序的,所以啊,需要先轉(zhuǎn)成一種線性的代碼,再生成低級代碼。我覺得 ast 也可以算一種樹形 IR,IR 是 immediate representation 中間表示的意思。要先把 AST 轉(zhuǎn)成線性 IR,然后再生成匯編、字節(jié)碼等。
咋翻譯,樹形的結(jié)構(gòu)咋變成線性的呢?明顯要遞歸啊,按照語法結(jié)構(gòu)遞歸 ast,進行每個節(jié)點的翻譯,這叫做語法制導(dǎo)翻譯,用線性 IR 中的指令來翻譯 AST 節(jié)點的屬性。每個節(jié)點的翻譯方式,if 咋翻譯、while 咋翻譯等可以去看下相關(guān)資料,搜中間代碼生成就好了。
但是 ast 不能上來就轉(zhuǎn)中間代碼。
昊昊:為啥,ast 不就能表示源碼信息了么,為啥不能直接翻譯成線性 ir?
我:因為還沒做語義檢查啊,結(jié)構(gòu)對不一定意思對,就像“昊昊是只豬”,這個符合語法吧,但是語義明顯不對啊,這不是罵人么,所以要先做語義檢查。還有就是要推導(dǎo)出一些信息來,才能做后續(xù)的翻譯。
語義分析要檢查出語義的錯誤,比如類型是否匹配、引用的變量是否存在、break 是否在 while 中等,主要要做作用域分析、引用消解、類型推導(dǎo)和檢查、正確性檢查等。
作用域分析就是分析函數(shù)、塊等,這些作用域內(nèi)的變量都有啥,作用域之間的聯(lián)系是怎樣的,其實作用域是一棵樹,從頂層作用域到子作用域可以生成一個樹形數(shù)據(jù)結(jié)構(gòu)。我記得有個做 scope 分析的 webpack 插件,他是把模塊也給鏈接起來了,形成了一個大的 scope graph,然后做分析。
作用域中有各種聲明,要把它們的類型、初始值、訪問修飾符等信息記錄下來,保存這個信息的結(jié)構(gòu)叫符號表,這相當(dāng)于是一個緩存,之后處理這個符號的時候直接去查符號表就行,不用再次從 ast 來找。
引用消解呢就是對每個符號檢查下是否都能查找到定義,如果查找不到就報錯。類型方面你比較熟,js 的源碼中肯定不可能都寫類型,很多地方可以直接推導(dǎo)出來,根據(jù) ast 可以得出類型的聲明,記錄到符號表中,之后遍歷 ast,對各種節(jié)點取出聲明時的類型來進行檢查,不一致就報錯。還有其他一些瑣碎的檢查,比如 continue、break 只能出現(xiàn)在 while 中等等一些檢查。
昊昊:語義分析我懂了,就是檢查錯誤和記錄一些分析出的信息到符號表,那語義分析之后呢?
語義分析之后就代表著程序已經(jīng)沒有語法和語義的錯誤了,可以放心進行各種后續(xù)轉(zhuǎn)換,不會再有開發(fā)者的錯誤。之后先翻譯成線性 IR,然后對線性 IR 進行優(yōu)化,需要優(yōu)化就是因為自動生成的代碼難免有很多冗余,需要把各種沒必要的處理去掉。但是要保證語義不變。比如死代碼刪除、公共子表達式刪除、常量傳播等等。
線性 IR 的分析要建立流圖,就是控制流圖,控制流就是根據(jù) if、while、函數(shù)調(diào)用等導(dǎo)致的程序跳轉(zhuǎn),把順序執(zhí)行的代碼和跳轉(zhuǎn)到的代碼之間連接起來就是一個圖,順序執(zhí)行的代碼看成一個整體,叫做基本快。之后根據(jù)這個流圖做數(shù)據(jù)流分析,也就是分析一個變量流經(jīng)了那些代碼,然后基于這些做各種優(yōu)化。
這個部分叫做程序分析,或者靜態(tài)分析,是一個專門的方向,可以用于代碼漏洞的靜態(tài)檢查,可以用于編譯優(yōu)化,這個是比較難的。研究這個的博士都比較少。國內(nèi)只有北大和南大開設(shè)程序分析課程。
優(yōu)化之后的線性 IR 就可以生成匯編代碼了,然后通過匯編器轉(zhuǎn)成機器碼,再鏈接一些標(biāo)準(zhǔn)庫,比如 v8 目錄下可以看到 builtins 目錄,這里就是各種編譯好的機器碼文件,可以靜態(tài)鏈接成一個可執(zhí)行文件。
昊昊:哦,感覺匯編和鏈接這兩步前端接觸不到啊。
我:對的,因為 js 是解釋型語言,直接從源碼解釋執(zhí)行,不要說 js 了,java 的字節(jié)碼也不需要靜態(tài)鏈接。像 c、c++這些生成可執(zhí)行文件的才需要通過匯編器把代碼專成機器碼然后鏈接成一個文件。而且如果目標(biāo)平臺有這些庫,那么不需要靜態(tài)鏈接到一起,可以動態(tài)鏈接。你可能聽過.dll 和.so 這就分別是 windows 和 linux 的用于運行時動態(tài)加載的保存機器碼的文件。
你說的沒錯,前端領(lǐng)域基本不需要匯編和鏈接,就算是 wasm,也是生成 wasm 字節(jié)碼,之后解釋執(zhí)行。前端主要還是轉(zhuǎn)譯器。
轉(zhuǎn)譯器是咋處理 AST 的?
昊昊:那轉(zhuǎn)譯器在 ast 之后又做了哪些處理呢?
我:轉(zhuǎn)譯器的目標(biāo)代碼也是高級語言,也是嵌套的結(jié)構(gòu),所以從高級語言到高級語言是從樹形結(jié)構(gòu)到樹形結(jié)構(gòu),不像翻譯成低級的指令方式組織的語言,還得先翻譯成線性 IR,高級到高級語言的轉(zhuǎn)換,只需要 ast,對 ast 做各種轉(zhuǎn)換之后,就可以做代碼生成了。
昊昊:我說呢,我就沒聽說 babel 中有線性 IR 的概念。
我:對的,不管是跨語言的轉(zhuǎn)換,比如 ts 轉(zhuǎn) rust,還是同語言的轉(zhuǎn)換 js 轉(zhuǎn) js 都不需要線性結(jié)構(gòu),兩棵樹的轉(zhuǎn)換要啥線性中間代碼啊。所以一般轉(zhuǎn)譯器都是 parse、transform、generate 這 3 個階段。
parse 廣義上來說包含詞法、語法和語義的分析,狹義的 parse 單指語法分析。這個不必糾結(jié)。
transform 就是對 ast 的增刪改,之后 generator 再把 ast 打印成字符串,我們解析 ast 的時候把[]{} () 等分隔符去掉了,generate 的時候再把細節(jié)加回來。
其實前端領(lǐng)域主要還是轉(zhuǎn)譯器,因為主流 js 引擎執(zhí)行的是源代碼,但是這個源代碼和我們寫的源代碼還不太一樣,所以前端很多源碼到源碼的轉(zhuǎn)譯器來做這種轉(zhuǎn)換,比如 babel、typescript、terser、eslint、postcss、prettier 等。
babel 是把高版本 es 代碼轉(zhuǎn)成低版本的,并且注入 polyfill。typescript 是類型檢查和轉(zhuǎn)成 js 代碼。eslint 是根據(jù)規(guī)范檢查,但--fix 也可以生成修復(fù)后的代碼。prettier 也是用于格式化代碼的,比 eslint 處理的更多,不只限于 js。postcss 主要是處理 css 的,posthtml 用于處理 html。相信你也用過很多了。taro 這種小程序轉(zhuǎn)譯器就是基于 babel 封裝的。
解釋器是咋處理 AST 的?
昊昊:哦,光哥,我大概知道編譯器和轉(zhuǎn)譯器都對 ast 做了啥處理了,這倆都是生成代碼的,那解釋器呢?
我:對,首先轉(zhuǎn)譯器也是編譯器的一種,只不過比較特殊,叫做 transpiler,一般的編譯器叫做 compiler。解釋器和編譯器的區(qū)別確實是是否生成代碼,提前編譯成機器代碼的叫做 AOT 編譯器,運行時編譯成機器代碼的叫做 JIT 編譯器,
解釋器并不生成機器代碼,那它是怎么執(zhí)行的呢?知道你肯定有疑問。其實解釋器是用一門高級語言來解釋另一門高級語言,比如 c++,一般都用 c++來寫解釋器,因為可以做內(nèi)存管理。用 c++來寫 js 解釋器,像 v8、spidermonkey 等都是。我們在有了 ast 并且做完語義分析之后就可以遍歷 ast,然后用 c++來執(zhí)行不同的節(jié)點了,這種叫做 tree walker 解釋器,直接解釋執(zhí)行 ast,v8 引擎在 17 年之前都是這么干的。但是在 17 年之后引入了字節(jié)碼,因為字節(jié)碼可以緩存啊,這樣下次再直接執(zhí)行字節(jié)碼就不需要 parse 了。字節(jié)碼是種線性結(jié)構(gòu),也要做 ast 到線性 ir 的轉(zhuǎn)換,之后在 vm 上執(zhí)行字節(jié)碼。
一般解釋線性代碼的比如匯編代碼、字節(jié)碼等這種的程序才叫做虛擬機,因為機器代碼就是線性的,其實從 ast 開始就可以解釋了,但是卻不叫 vm,我覺得就是因為這個,和機器碼比較像的線性代碼的解釋器才叫 vm。
不管是解釋 ast 也好,還是轉(zhuǎn)成字節(jié)碼再解釋也好,效率都不會特別高,因為是用別的高級語言來執(zhí)行當(dāng)前語言的代碼,所以要提高效率還是得編譯成機器代碼,這種運行時編譯就是 JIT 編譯器,編譯是耗時的,所以也不是啥代碼都 JIT,要做熱度的統(tǒng)計,到達了閾值才會做 JIT。然后把機器碼緩存下來,當(dāng)然也可能是緩存的匯編代碼,用到的時候再用匯編器轉(zhuǎn)成機器碼,因為機器代碼占的空間比較大。
可以對比 v8 來理解,v8 有 parser、ignation 解釋器、turbofan 編譯器,還有 gc。ignation 解釋器就是把 parse 出的 ast 轉(zhuǎn)成字節(jié)碼,然后解釋執(zhí)行字節(jié)碼,熱度到達閾值之后會交給 turbofan 編譯為匯編代碼之后生成機器代碼,來加速。gc 是獨立的做內(nèi)存管理的。
turbofan 是渦輪增壓器,這個名字就能體現(xiàn)出 JIT 的意義。但 JIT 提升了執(zhí)行速度,也有缺點,比如會使得 js 引擎體積更大,占用內(nèi)存更大,所以輕量級的 js 引擎不包含 jit,這就是運行速度和包大小、內(nèi)存空間之間的權(quán)衡。架構(gòu)設(shè)計也經(jīng)常要做這種兩邊都可以,但是要做選擇的 trade off,我們叫做方案勾兌。
說到權(quán)衡,我想起 rn 的 js 引擎 hermes 就改成支持直接執(zhí)行字節(jié)碼了,在編譯期間把 js 代碼編譯成字節(jié)碼,然后直接執(zhí)行字節(jié)碼,這就是在跨端領(lǐng)域的 js 引擎的 trade off。
前端領(lǐng)域都有哪些地方用到編譯知識?
昊昊:哦,光哥,我明白解釋器、編譯器、轉(zhuǎn)譯器都干啥的了,那前端領(lǐng)域都有那些地方用到編譯原理的知識呢?
我:其實你也肯定有個大概的了解了,但是不夠明確,我列一下我知道的。
工程化領(lǐng)域各種轉(zhuǎn)譯器:babel、typescript、eslint、terser、prettier、postcss、posthtml、taro、vue template compiler 等
js 引擎:v8、javascriptcore、quickjs、hermes 等
wasm:llvm 可以生成 wasm 字節(jié)碼,所以 c++、rust 等可以轉(zhuǎn)為 llvm ir 的語言都可以做 wasm 開發(fā)
ide 的 lsp:編程語言的語法高亮、智能提示、錯誤檢查等通過 language service protocol 協(xié)議來通信,而 lsp 服務(wù)端主要是基于 parser 對正在編輯的文本做分析
自己如何實現(xiàn)一門語言呢?
昊昊:我學(xué)了編譯原理可以實現(xiàn)一門語言么?
我:其實編程語言主要還是設(shè)計,實現(xiàn)的話首先實現(xiàn) parser 和語義分析,后面分為兩條路,一種是解釋執(zhí)行的解釋器配合 JIT 編譯器的路,一種是編譯成匯編代碼碼,然后生成機器碼再鏈接成可執(zhí)行文件的編譯器的路。
parser 部分比較繁瑣,可以用 antlr 這種 parser 生成器來生成,語義分析要自己寫,這個不太難,主要是對 ast 的各種處理。之后如果想做成編譯器,可以用 llvm 這種通用的優(yōu)化器和代碼生成器,clang、rust、swift 都是基于它,所以很靠譜,可以直接用。如果做解釋器可以寫 tree walker 解釋器,或者再進一步生成線性字節(jié)碼,然后寫個 vm 來解釋字節(jié)碼。JIT 編譯器也可以用 llvm 來做。要把 ast 轉(zhuǎn)成 llvm ir,也是樹形結(jié)構(gòu)轉(zhuǎn)線性結(jié)構(gòu),這個還是編譯領(lǐng)域很常見的操作。
其實編譯原理只是告訴你怎么去實現(xiàn),語言設(shè)計不關(guān)心實現(xiàn),一門語言可以實現(xiàn)為編譯型也可以實現(xiàn)為解釋型,也可以做成 java 那種先編譯后解釋,你看 hermes 不就是先把 js 編譯為字節(jié)碼然后解釋執(zhí)行字節(jié)碼么。語言不分編譯解釋,這個概念要有,c 也有解釋器,js 也有編譯器,我們說一門語言是編譯型還是解釋型主要是主流的方式是編譯還是解釋來決定的。
編程語言可以分為 GPL 和 DSL 兩種。
GPL 是通用編程語言,它是圖靈完備的,也就是能夠描述任何可計算問題,像 c++、java、python、go、rust 等這些語言都是圖靈完備的,所以一門語言能實現(xiàn)的另一門語言都能實現(xiàn),只不過實現(xiàn)難度不同。比如 go 語言內(nèi)置協(xié)程實現(xiàn),那么寫高并發(fā)程序就簡單,java 沒有語言級別的協(xié)程,那么就要上層來實現(xiàn)。你可能聽到過設(shè)計模式是對語言缺陷的補充就是這個意思,不同語言設(shè)計思路不同,內(nèi)置的東西也不同,有的時候需要運行時來彌補。、
編程語言有不同的設(shè)計思路,大的方向是編程范式,比如命令式、聲明式、函數(shù)式、邏輯式等,這些大的思路會導(dǎo)致語言的語法,內(nèi)置的實現(xiàn)都不同,表達能力也不同。這基本確定了語言基調(diào),后續(xù)再補也很難,就像 js 里面實現(xiàn)函數(shù)式,你又不能限制人家不能用命令式,就很難寫出純粹的函數(shù)式代碼。
DSL 不是圖靈完備的,卻換取了某領(lǐng)域的更強的表達能力,比如 html、css、正則表達式,jq 的選擇器語法等等,比較像一種偽代碼,特定領(lǐng)域的表達能力很強,但是卻不是圖靈完備的不能描述所有可計算問題。
編譯原理是實現(xiàn)編程語言的步驟要學(xué)習(xí)的,更上層的語言設(shè)計還要學(xué)很多東西,最好能熟悉多門編程語言的特性。
我該怎么學(xué)習(xí)編譯原理呢?
昊昊:光哥,那我該怎么學(xué)習(xí)編譯原理呢?
我:首先你要理解編譯都學(xué)什么,看我上面對編譯、轉(zhuǎn)譯、解釋的科普大概能有個印象,然后查下相關(guān)資料。知道都可以干啥了之后先寫 parser,因為不管啥都要先 parse 成 ast 才能被“理解”和后續(xù)處理,學(xué)下有限狀態(tài)機來分詞和遞歸下降構(gòu)造 ast。推薦看下 wepack 的 wxml transpiler,lexer 部分寫的還可以。這種 xml 的 parser 比較簡單,適合入門。語言級別的 parser 細節(jié)很多,還是得找一個來 debug 看。不過我覺得沒太大必要,一般也就寫個 html parser,要是語言的,可以用 antlr 生成。轉(zhuǎn)譯器肯定要了解 babel,這個是前端領(lǐng)域很不錯的轉(zhuǎn)譯器。
js 引擎可以嘗試用 babel 做 parser,自己做語義分析,解釋執(zhí)行 ast 試試,之后進一步生成字節(jié)碼或其他線性 ir,然后寫個 vm 來解釋字節(jié)碼。
還可以學(xué)習(xí) wasm 相關(guān)技術(shù),那個是涉及到其他語言編譯到 wasm 字節(jié)碼的過程的。
當(dāng)你學(xué)完了編譯原理,就大概知道怎么實現(xiàn)一門編程語言了,之后想深入語言設(shè)計可以多學(xué)一些其他編程范式的語言,了解下各種語言特性,怎么設(shè)計一門表達性強的 gpl 或者 dsl。也可以進一步學(xué)習(xí)一下操作系統(tǒng)和體系結(jié)構(gòu),因為編譯以后的代碼還是要在操作系統(tǒng)上以進程的形式運行的,那么運行時該怎么設(shè)計就要了解操作系統(tǒng)了。然后 cpu 指令集是怎么用電路實現(xiàn)的,這個想深入可以去看下計算機體系結(jié)構(gòu)。
不過,前端工程師不需要達到那種深度,但是眼界開闊點沒啥壞處。
作者:神說要有光
原文地址:https://zhuanlan.zhihu.com/p/356806174
參考資料
[1]
知乎-神說要有光: https://www.zhihu.com/people/di-xu-guang-50
