【譯】超級微型編譯器

引子

在看 babel 文檔的時候,接觸到 The Super Tiny Compiler,其中的註釋感受解釋的蠻容易理解,翻譯記錄一下。javascript

爲何要關注編譯器

大部分的人在他們的平常工做中,實際上沒有必要去思考編譯器相關的東西,不關注編譯器很正常。然而,編譯器在你的身邊很常見,你使用的不少工具,都是借鑑了編譯器的概念。java

編譯器很可怕

編譯器的確很可怕。可是這是咱們(那些寫編譯器的人)本身的錯誤,咱們捨棄了簡單合理,而且讓它變得如此複雜可怕,以致於大部分的人認爲是徹底沒法接近的事情,只有書呆子能夠明白。node

該從那裏開始?

從編寫一個最簡單的編譯器開始。這個編譯器很是的小,若是你移除全部的註釋,也只有 200 行代碼。git

咱們準備寫一個編譯器,它的做用是將一些 LISP 方法調用的形式轉換成 C 語言裏面方法調用的的形式。github

若是你對其中的語言不太熟悉,我將會簡單介紹一下。數組

若是咱們有兩個方法 addsubtract,它們會像下面這樣書寫: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

  • 解析:獲取每一行代碼,並將其變成更加抽象的代碼。
  • 轉換:獲取抽象的代碼,按照編譯器的意圖進行操做。
  • 代碼生成:獲取轉換後表現的代碼,並將其變換成新的代碼。

解析(Parsing)

解析一般分爲兩個階段:詞法分析(Lexical Analysis)和語法分析(Syntactic Analysis)。翻譯

  • 詞法分析:獲取代碼,而且用分詞器(tokenizer)將其分解成單獨的記號(tokens)。記號是一個數組,裏面是描述語法各個部分的對象。它們多是數字、文本、標點符號、操做符等等。
  • 語法分析:獲取那些記號,並將其轉換爲另一種表現形式,這種表現形式描述了本身的語法和相互聯繫。這被稱爲中間表示或抽象語法樹(Abstract Syntax Tree 或 AST)。抽象語法樹是一個深層嵌套的對象,表明代碼運行的一種方式,它提供給咱們不少信息。

例以下面的語法:

(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',
      }]
    }]
  }]
}

轉換(Transformation)

編譯的下一個階段就是轉換。這一步只是獲取上一步的 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,咱們將這樣遍歷:

  1. Program - 從 AST 最頂層級開始
  2. CallExpression (add) - 移動到 Program 的 body 裏面的第一個元素
  3. NumberLiteral (2) - 移動到 CallExpression(add) 的 params 的第一個元素
  4. CallExpression (subtract) - 移動到 CallExpression(add) 的 params 的第二個元素
  5. NumberLiteral (4) - 移動到 CallExpression(subtract) 的 params 的第一個元素
  6. NumberLiteral (2) - 移動到 CallExpression(subtract) 的 params 的第二個元素

若是直接操做這個 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) {},
  }
};

代碼生成(Code Generation)

編譯的最後一個階段就是代碼生成。有些時候編譯器在這個階段,會作跟轉換重疊的事情,可是大部分的代碼生成只是意味着獲取 AST 而且轉換成字符串代碼。

代碼生成有幾種不一樣的運行方式,一些編譯器會重用以前的記號(tokens),有些會建立一個單獨的代碼表示,這樣他們就能夠線性打印節點,可是從我瞭解到的狀況,大部分會使用咱們剛纔建立的 AST,也是咱們將要關注的。

咱們的代碼生成器將有效地知道如何「打印(print)」 AST 的全部不一樣節點類型,而且它將遞歸地調用本身來打印嵌套的節點,直到將全部內容打印成一長串代碼。

就這樣!這些就是編譯器全部不一樣的部分。並非每個編譯器都像我這裏描述的那樣。編譯器用於不一樣的目的,它們可能須要比我描述的更多的步驟。可是,如今你應該對於大部分編譯器是什麼樣的,有一個更高的認識。

如今,我已經解釋了這麼多,你應該都能很好的寫出本身的編譯器,對吧?只是開個玩笑,這就是我要幫助的。那麼就讓咱們開始吧!

編譯器示例

the-compiler-js

參考資料

相關文章
相關標籤/搜索