在看 babel 文檔的時候,接觸到 The Super Tiny Compiler,其中的註釋感受解釋的蠻容易理解,翻譯記錄一下。javascript
大部分的人在他們的平常工做中,實際上沒有必要去思考編譯器相關的東西,不關注編譯器很正常。然而,編譯器在你的身邊很常見,你使用的不少工具,都是借鑑了編譯器的概念。java
編譯器的確很可怕。可是這是咱們(那些寫編譯器的人)本身的錯誤,咱們捨棄了簡單合理,而且讓它變得如此複雜可怕,以致於大部分的人認爲是徹底沒法接近的事情,只有書呆子能夠明白。node
從編寫一個最簡單的編譯器開始。這個編譯器很是的小,若是你移除全部的註釋,也只有 200 行代碼。git
咱們準備寫一個編譯器,它的做用是將一些 LISP 方法調用的形式轉換成 C 語言裏面方法調用的的形式。github
若是你對其中的語言不太熟悉,我將會簡單介紹一下。數組
若是咱們有兩個方法 add
和 subtract
,它們會像下面這樣書寫:babel
example | LISP | C |
---|---|---|
2 + 2 | (add 2 2) | add(2, 2) |
4 - 2 | (subtract 4 2) | subtract(4, 2) |
2 + (4 - 2) | (add 2 (subtract 4 2)) | add(2, subtract(4, 2)) |
很容易,對吧?很好,這正是咱們準備要編譯的。雖然這個不是完整的 LISP 或 C 語法,但它的語法足以演示大部分現代編譯器的主要部分。工具
大部分的編譯能夠劃分爲 3 個主要階段:解析(Parsing),轉換(Transformation),代碼生成(Code Generation)。ui
解析一般分爲兩個階段:詞法分析(Lexical Analysis)和語法分析(Syntactic Analysis)。翻譯
例以下面的語法:
(add 2 (subtract 4 2))
記號看起來可能像這樣:
[ { type: 'paren', value: '(' }, { type: 'name', value: 'add' }, { type: 'number', value: '2' }, { type: 'paren', value: '(' }, { type: 'name', value: 'subtract' }, { type: 'number', value: '4' }, { type: 'number', value: '2' }, { type: 'paren', value: ')' }, { type: 'paren', value: ')' }, ]
抽象語法樹看起來可能像這樣:
{ type: 'Program', body: [{ type: 'CallExpression', name: 'add', params: [{ type: 'NumberLiteral', value: '2', }, { type: 'CallExpression', name: 'subtract', params: [{ type: 'NumberLiteral', value: '4', }, { type: 'NumberLiteral', value: '2', }] }] }] }
編譯的下一個階段就是轉換。這一步只是獲取上一步的 AST 而且再一次改變它。能夠用相同的語言操做 AST,或者把 AST 轉換成一個徹底新的語言。
讓咱們看看若是轉換一個 AST。
你可能發現咱們的 AST 裏面有些元素很類似。這裏有一些擁有 type
屬性的對象,每個這樣的對象被稱爲 AST 節點(AST Node)。這些節點定義了樹上每一個單獨部分的屬性。
咱們有一個 NumberLiteral
節點:
{ type: 'NumberLiteral', value: '2', }
或者多是一個 CallExpression
節點:
{ type: 'CallExpression', name: 'subtract', params: [...nested nodes go here...], }
當轉換 AST 時,咱們能夠對節點的屬性進行添加/移除/替換操做,咱們能夠添加新的節點,移除節點,或者基於已存在的 AST 建立一個徹底新的 AST。
由於咱們的目標是一個新的語言,因此咱們將要針對新的語言,建立一個徹底新的 AST。
遍歷(Traversal)
爲了可以找到全部的節點,咱們須要遍歷它們。這個遍歷的過程要到達 AST 的每個節點。
{ type: 'Program', body: [{ type: 'CallExpression', name: 'add', params: [{ type: 'NumberLiteral', value: '2' }, { type: 'CallExpression', name: 'subtract', params: [{ type: 'NumberLiteral', value: '4' }, { type: 'NumberLiteral', value: '2' }] }] }] }
上面的 AST,咱們將這樣遍歷:
若是直接操做這個 AST,這裏可能要介紹各類抽象。可是咱們正在嘗試作的事情,訪問到樹的每一個節點就足夠了。
訪問者(Visitors)
這裏基本的思路是,建立一個「visitor」對象,它擁有的方法能夠接受不一樣類型的節點。
var visitor = { NumberLiteral() {}, CallExpression() {}, };
當咱們遍歷 AST 時,只要「進入(enter)」到一個匹配的類型節點,咱們將調用這個 visitor 的方法。
爲了讓這個想法可行,咱們將傳入一個節點和其父節點的引用。
var visitor = { NumberLiteral(node, parent) {}, CallExpression(node, parent) {}, };
而後,這裏還存在「退出(exit)」的可能性。想象一下咱們這樣的樹結構:
- Program - CallExpression - NumberLiteral - CallExpression - NumberLiteral - NumberLiteral
當咱們遍歷下去,最終會到達一個死衚衕。因此當咱們完成樹每一個分支的遍歷,咱們就「退出(exit)」。所以,向下遍歷樹,「進入(enter)」到樹節點,返回的時候,咱們就「退出(exit)」。
-> Program (enter) -> CallExpression (enter) -> Number Literal (enter) <- Number Literal (exit) -> Call Expression (enter) -> Number Literal (enter) <- Number Literal (exit) -> Number Literal (enter) <- Number Literal (exit) <- CallExpression (exit) <- CallExpression (exit) <- Program (exit)
爲了支持這種功能,最後咱們的 visitor 看起來會像是這樣:
var visitor = { NumberLiteral: { enter(node, parent) {}, exit(node, parent) {}, } };
編譯的最後一個階段就是代碼生成。有些時候編譯器在這個階段,會作跟轉換重疊的事情,可是大部分的代碼生成只是意味着獲取 AST 而且轉換成字符串代碼。
代碼生成有幾種不一樣的運行方式,一些編譯器會重用以前的記號(tokens),有些會建立一個單獨的代碼表示,這樣他們就能夠線性打印節點,可是從我瞭解到的狀況,大部分會使用咱們剛纔建立的 AST,也是咱們將要關注的。
咱們的代碼生成器將有效地知道如何「打印(print)」 AST 的全部不一樣節點類型,而且它將遞歸地調用本身來打印嵌套的節點,直到將全部內容打印成一長串代碼。
就這樣!這些就是編譯器全部不一樣的部分。並非每個編譯器都像我這裏描述的那樣。編譯器用於不一樣的目的,它們可能須要比我描述的更多的步驟。可是,如今你應該對於大部分編譯器是什麼樣的,有一個更高的認識。
如今,我已經解釋了這麼多,你應該都能很好的寫出本身的編譯器,對吧?只是開個玩笑,這就是我要幫助的。那麼就讓咱們開始吧!