JavaScript 實現超小型語法編譯器

前言

本篇文章是筆者精讀 the-super-tiny-compiler 的源代碼後的總結,筆者特別推薦你們去讀,不然看此篇文章容易一頭霧水。javascript

首先,創建對 ast 抽象語法樹的初步瞭解,你們能夠在 astexplorer 這個網站上輸入一段 javascript 代碼,在右側面板中查看生成的 ast 語法樹。css

好比輸入 add(3, div(8, 2)) (解析器默認挑選了 acorn))解析出來的抽象語法樹 ast 以下右側: 前端

這多是 ast 目前給你們最直觀的印象。如下將用圖解介紹如何從一句簡單的源代碼(haskell),經過編譯、生成、分析 ast,轉換成新的語言(javascript)。java

背景

首先,做爲一個前端工程師,筆者以爲了解編譯原理,有如下幾個好處:node

  1. 編譯原理的確是一個頗有趣的知識,學習一下純當愛好。
  2. 多瞭解相關的編譯知識,有助於加深對編程語言的理解。
  3. 擴展開來,對 javascript 轉譯、代碼壓縮、eslint、css 預處理器、prettier 等工具的理解和使用都會有幫助。

目標

本文的目標,將一段 haskell 語法的代碼經過自制的小型編譯器編譯後解析爲 javascript 語法的代碼。以下:git

const input = "(add 3 (div 8 2))";
const output = "add(3, div(8, 2))";
複製代碼

編寫一個函數 compiler 使得 input 轉換爲 output, 示意圖以下:github

那麼咱們通常對程序編譯會走通過幾個過程呢?通常來講,簡單可分爲詞法分析,語法分析和代碼生成三步。如下將會補充描述。express

詞法分析 - Tokenizer

詞法分析,目標將一段代碼分割成一個個的單詞和標點符號。編程

詞法分析的函數也叫作掃描 scanner。掃描器讀取代碼後,依據預約的規則合併生成成一個個的標識 tokens(移除空白符,註釋)。生成 tokens 列表。前端工程師

示意圖以下:

數據結構以下:

[
  { type: "paren", value: "(" },
  { type: "name", value: "add" },
  { type: "number", value: "3" },
  { type: "paren", value: "(" },
  { type: "name", value: "div" },
  { type: "number", value: "8" },
  { type: "number", value: "2" },
  { type: "paren", value: ")" },
  { type: "paren", value: ")" }
];
複製代碼

語法分析 - Parser

語法分析,主要目的在於經過分析詞法分析生成的詞法單元流,構建出一個表明當前程序的抽象語法樹 ast。

由上面的 tokenizer 進行分詞後,其實並無表達出語法。此時須要設計一個 parser 將 tokens 轉換爲 ast。parser 指針將逐個解析 token,在以上簡化的 tokens 列表中,parser 在解析到左括號 "(" 此 token 時,進入遞歸,只要下一個 token 不是右括號 ")",就不會結束遞歸。

最終指針移至 tokens 列表尾部,消費完成全部 tokens,此時生成完整的 ast。

示意圖以下:

數據結構以下:

{
  "type": "Program",
  "body": [
    {
      "type": "CallExpression",
      "name": "add",
      "params": [
        {
          "type": "NumberLiteral",
          "value": "3"
        },
        {
          "type": "CallExpression",
          "name": "div",
          "params": [
            {
              "type": "NumberLiteral",
              "value": "8"
            },
            {
              "type": "NumberLiteral",
              "value": "2"
            }
          ]
        }
      ]
    }
  ]
}
複製代碼

所以,經過遞歸生成了以上的 CallExpression 嵌套的模塊。

代碼轉換 - Transformer

這裏主要的工做,經過語法映射,將 haskell 的抽象語法樹轉成 javascript 的抽象語法樹。這裏有一個很重要的概念就是 traverser。這是一個能夠經過匹配類型從而遍歷訪問 ast 某個節點的工具函數。

traverse(ast, {
  Program: {
    enter(node, parent) {
      // ...
    },
    exit(node, parent) {
      // ...
    }
  },

  CallExpression: {
    enter(node, parent) {
      // ...
    },
    exit(node, parent) {
      // ...
    }
  },

  NumberLiteral: {
    enter(node, parent) {
      // ...
    },
    exit(node, parent) {
      // ...
    }
  }
});
複製代碼

traverse 的第一個參數是 ast,第二個參數咱們稱做 visitor(訪問器)。咱們定義了每一個 type 的 visitor。在 traverse 遞歸遍歷每一個節點的時候,都會在對應的時機執行 enter、exit 回調函數。

有了上述的 traverse 工具後,咱們能夠在訪問每一個節點的時候,不斷生成、並調整咱們的新語法樹。

最終通過 transformer 轉換後的 javascript 的 ast 語法樹數據結構以下:

{
  "type": "Program",
  "body": [
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "CallExpression",
        "callee": {
          "type": "Identifier",
          "name": "add"
        },
        "arguments": [
          {
            "type": "NumberLiteral",
            "value": "3"
          },
          {
            "type": "CallExpression",
            "callee": {
              "type": "Identifier",
              "name": "div"
            },
            "arguments": [
              {
                "type": "NumberLiteral",
                "value": "8"
              },
              {
                "type": "NumberLiteral",
                "value": "2"
              }
            ]
          }
        ]
      }
    }
  ]
}
複製代碼

代碼生成 - Generator

代碼生成器主要的職責是將轉換後的 ast 經過特定規則組合爲新的代碼。

在獲得經過 transformer 轉換後的新語法樹後,代碼生成器一樣遞歸的調用本身打印 ast 中的每個節點,最終生成一個長長的代碼字符串。也就是咱們目標的:output。

小結

筆者對編譯原理了解很少,此文興趣所致,描述有可能不甚嚴謹。之後若有機會學習深刻編譯原理,會繼續發文補充。

以上,對你們若有助益,不勝榮幸。

參考資料

相關文章
相關標籤/搜索