本篇文章是筆者精讀 the-super-tiny-compiler 的源代碼後的總結,筆者特別推薦你們去讀,不然看此篇文章容易一頭霧水。javascript
首先,創建對 ast 抽象語法樹的初步瞭解,你們能夠在 astexplorer 這個網站上輸入一段 javascript 代碼,在右側面板中查看生成的 ast 語法樹。css
好比輸入 add(3, div(8, 2))
(解析器默認挑選了 acorn))解析出來的抽象語法樹 ast 以下右側: 前端
這多是 ast 目前給你們最直觀的印象。如下將用圖解介紹如何從一句簡單的源代碼(haskell),經過編譯、生成、分析 ast,轉換成新的語言(javascript)。java
首先,做爲一個前端工程師,筆者以爲了解編譯原理,有如下幾個好處:node
本文的目標,將一段 haskell 語法的代碼經過自制的小型編譯器編譯後解析爲 javascript 語法的代碼。以下:git
const input = "(add 3 (div 8 2))"; const output = "add(3, div(8, 2))"; 複製代碼
編寫一個函數 compiler 使得 input 轉換爲 output, 示意圖以下:github
那麼咱們通常對程序編譯會走通過幾個過程呢?通常來講,簡單可分爲詞法分析,語法分析和代碼生成三步。如下將會補充描述。express
詞法分析,目標將一段代碼分割成一個個的單詞和標點符號。編程
詞法分析的函數也叫作掃描 scanner。掃描器讀取代碼後,依據預約的規則合併生成成一個個的標識 tokens(移除空白符,註釋)。生成 tokens 列表。markdown
示意圖以下:
數據結構以下:
[ { 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: ")" } ]; 複製代碼
語法分析,主要目的在於經過分析詞法分析生成的詞法單元流,構建出一個表明當前程序的抽象語法樹 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 嵌套的模塊。
這裏主要的工做,經過語法映射,將 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" } ] } ] } } ] } 複製代碼
代碼生成器主要的職責是將轉換後的 ast 經過特定規則組合爲新的代碼。
在獲得經過 transformer 轉換後的新語法樹後,代碼生成器一樣遞歸的調用本身打印 ast 中的每個節點,最終生成一個長長的代碼字符串。也就是咱們目標的:output。
筆者對編譯原理了解很少,此文興趣所致,描述有可能不甚嚴謹。之後若有機會學習深刻編譯原理,會繼續發文補充。
以上,對你們若有助益,不勝榮幸。