摘要:手把手教你實現一個簡單的編譯器概述今天我們將學習開發一個編譯器,但是呢,這個編譯器并不是說什么都能都編譯,它只是一個超級小的編譯器,主要用于說明編譯器的一些基本的原理。這被稱為中間表示或抽象語法樹。
手把手教你實現一個簡單的編譯器 1、 概述
今天我們將學習開發一個編譯器,但是呢,這個編譯器并不是說什么都能都編譯,它只是一個超級小的編譯器,主要用于說明編譯器的一些基本的原理。
我們這個編譯器可以將類似于lisp語言的函數調用編譯成類似于C語言的函數調用。如果你對lisp語言和C語言這兩者都不熟悉,沒關系,什么語言其實無所謂,但接下來還是會給你一個快速的介紹。
如果我們有兩個函數分別是add和subtract,如果用它們來計算下面的表達式:
2 + 2 4 - 2 2 + (4 - 2)
那么在lisp語言中它可能長這樣子:
(add 2 2) // 2 + 2 (subtract 4 2) // 4 - 2 (add 2 (subtract 4 2)) // 2 + (4 - 2)
而在C語言中它長這個樣子:
add(2, 2) subtract(4, 2) add(2, subtract(4, 2))
相當簡單吧?
好吧,這是因為這僅僅只是我們這個編譯器所需要處理的情形。 這既不是list語言的完整語法,也不是C語言的完整語法。 但這點語法已經足以用來演示現代編譯器所做的大部分工作。
大部分編譯器所做的工作都可以分解為三個主要的步鄹: 解析、轉換和代碼生成。
解析。 解析就是將原始代碼轉換成代碼的抽象表示。
轉換。 轉換就是以這個抽象表示為基礎,做編譯器想做的任何事情。
代碼生成。 代碼生成就是將轉換后的抽象表示變成新的代碼。
2、 解析解析通常分為兩個階段:詞法分析和句法分析。
詞法分析。 詞法分析通常是使用一個標記器(或詞法分析器)將原始代碼拆分成叫做標記的東西。而標記是一些微小的對象組成的數組,它們通常用來描述一些孤立的語法片段,它們可以是數字、標簽、標點符號、操作符等等。
語法分析。 語法分析將詞法分析得到的標記重新格式化為用于描述語法的每個部分及其相互關系的表示。 這被稱為中間表示或抽象語法樹(AST)。抽象語法樹(簡稱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: ")" }, ]
然后它對應的抽象語法樹(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", }] }] }] }3、 轉換
在解析之后,編譯器的下一步鄹是轉換。 同樣,這不過就是將最后一步的抽象語法樹(AST)拿過來對它做一定的改變。這種改變多種多樣,可以是在同一種語言中進行改變,也可以直接將抽象語法樹轉換成另外一種完全不同的新語言。
讓我們來看看我們將如何轉換一個抽象語法樹(AST)。
你可能已經注意到,我們的抽象語法樹里面有一些非常類似的元素。 這些元素對象有一個type屬性。 這每一個對象元素都被稱為一個AST節點。 這些節點上定義的屬性用于描述AST樹上的一個獨立部分。
我們可以為數字字面量(NumberLiteral)建立一個節點:
{ type: "NumberLiteral", value: "2", }
或者是為調用表達式(CallExpression)創建一個節點:
{ type: "CallExpression", name: "subtract", params: [...nested nodes go here...], }
當轉換AST樹的時候,我們可能需要對它進行add、remove、replace等操作。 我們可以增加新節點,刪除節點或者我們完全可以將AST樹擱一邊不理,然后基于它創建一個全新的AST。
由于我們這個編譯器的目標是將lisp語言轉換成C語言,所以我們會聚焦創建一個專門用于目標語言(在這里是C語言)的全新AST。
3.1 遍歷為了瀏覽所有這些節點,我們需要能夠遍歷它們。 這個遍歷過程是對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,我們需要像這樣走:
Program - 從AST樹的頂層開始。
CallExpression (add) - 移動到Program的body屬性的第一個元素。
NumberLiteral (2) - 移動到CallExpression(add)的第一個參數。
CallExpression (subtract) - 移動到CallExpression(add)的第二個參數。
NumberLiteral (4) - 移動到CallExpression(subtract)的第一個參數。
NumberLiteral (2) - 移動到CallExpression(subtract)的第二個參數。
如果我們直接操作這個AST而不是創建一個多帶帶的AST,我們可能需要在這里引入各種抽象概念。 但是我們正在嘗試做的事情,只需要訪問樹中的每個節點就足夠了。
使用“訪問”這個詞的原因是因為這個詞能夠很好的表達如何在對象結構上操作元素。
3.2 訪問者這里最基本的思路就是我們創建一個訪問者對象,這個對象擁有一些方法,這些方法可以接受不同的節點類型。
比如下面這樣:
var visitor = { NumberLiteral() {}, CallExpression() {}, };
當我們遍歷AST的時候,一旦我們碰到一個與指定類型相匹配的節點,我們就會調用訪問者對象上的方法。
為了讓這個函數比較好用,我們給它傳遞了該節點以及它的父節點:
var visitor = { NumberLiteral(node, parent) {}, CallExpression(node, parent) {}, };
然而,這里也會有可能出現在退出時調用東西。 想象一下我們前面提到的樹結構:
- Program - CallExpression - NumberLiteral - CallExpression - NumberLiteral - NumberLiteral
當我們往下遍歷的時候,我們會遇到最終的分支。 當我們訪問完所有的分支后我們退出。 所以向下遍歷樹,我們進入節點,然后向上回溯的時候我們退出節點。
-> 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)
為了支持這種方式,我們的訪問者對象需要改成下面這個樣子:
var visitor = { NumberLiteral: { enter(node, parent) {}, exit(node, parent) {}, } };4、 代碼生成
編譯器的最后一步是代碼生成。有時候編譯器在這一步會重復做一些轉換步鄹做過的事情。 但是對代碼生成而言,它所做的大部分工作就是將我們的AST樹stringify一下輸出,也就是轉換成字符串輸出。
代碼生成有多種工作方式,有一些編譯器會重復利用前面生成的標記,另一些編譯器會創建代碼的多帶帶表示,以便線性地打印節點,但是據我說知,大多數編譯器的策略是使用我們剛剛創建的那個AST,這是我們將要關注的。
實際上,我們的代碼生成器將知道如何打印AST的所有不同節點類型,并且它將遞歸地調用自己來打印嵌套節點,直到將所有內容打印成一長串代碼。
而就是這樣! 這就是編譯器的所有差異部分。
現在不是說每個編譯器看起來都和我在這里描述的完全一樣。 編譯器有許多不同的用途,他們可能需要比我詳細的更多的步驟。
但是現在您應該對大多數編譯器的輪廓有一個總體的高層次的概念。
現在我已經解釋了所有這些,你應該可以寫好自己的編譯器了是吧?
只是在開玩笑的啦,我會在這里繼續提供幫助,所以我們開始吧!
5、編譯器的代碼實現前面說了,整個編譯器大概可以分為三步:解析、轉換、代碼生成。而解析又可以分成兩步:詞法解析和句法解析。所以一共需要四個函數就可以實現了。我們來分別看一下:
5.1、 解析的實現 5.1.1、 詞法解析之tokenizer實現我們將從編譯器的第一步——解析——開始,利用tokenizer函數進行詞法分析。
我們將把字符串代碼拆分成由標記組成的數組:
(add 2 (subtract 4 2)) => [{ type: "paren", value: "(" }, ...]
我們的tokenizer接收一個代碼字符串, 然后接下來做兩個事情:
function tokenizer(input) { // 一個current變量,類似于游標,用于跟蹤我們在代碼字符串中的位置 let current = 0; // 以及一個tockens數組,用于將我們分解的標記存放其中 let tokens = []; // 我們創建一個while循環,在這里面我們設置我們的current變量,這個變量會隨著循環的深入而不斷增加 // // 這么做是因為tockens可能會是任意長度 while (current < input.length) { // 我們還會存儲變量current所在位置的字符 let char = input[current]; // 我們首先要檢查的是左括弧,這個將會在稍后用于CallExpression,但是此時我們只關心左括弧字符 // // 我們檢查看看有沒有左括弧: if (char === "(") { // 如果有,則建立一個對象,其type屬性是paren,值為左括弧, 然后我們將這個對象加入tokens數組 tokens.push({ type: "paren", value: "(", }); // 接著我們增加current變量,也就是移動游標 current++; // 然后進行下一輪循環. continue; } // 接著我們檢查右括弧,我們按照前面的套路來做:檢查右括弧,新增一個標記,增加current, 進行下一輪循環 if (char === ")") { tokens.push({ type: "paren", value: ")", }); current++; continue; } // 接著,我們檢查空白格。 這很有趣,因為我們關注空白格是因為它將字符串分隔開,但是我們并不需要將空白格存為標記,我們 // 可以直接扔掉它,所以這里我們僅僅檢查空白格是否存在,如果它存在我們就進入下一輪循環 let WHITESPACE = /s/; if (WHITESPACE.test(char)) { current++; continue; } // 下一個類型的標記是數字,這和我們前面見到的不同,因為一個數字可能是任意個字符組成,并且我們需要捕獲整個字符序列作為一個標記 // // (add 123 456) // ^^^ ^^^ // 比如上面的就只有兩個獨立的數字標記 // // 所以當我們遇到序列中的第一個數字的時候開始進一步處理. let NUMBERS = /[0-9]/; if (NUMBERS.test(char)) { // 我們在這里面創建了一個value字符,用于拼接數字字符 let value = ""; // 接下來我們遍歷后面的每一個字符直到遇到一個非數字字符,將這些字符和前面的value變量拼接起來, 并且改變current游標 while (NUMBERS.test(char)) { value += char; char = input[++current]; } // 這之后我們將創建數字標記并加入tokens數組 tokens.push({ type: "number", value }); // 然后我們繼續 continue; } // 我們也支持字符串,字符串就是用雙引號(")包裹的一段文本,比如 // // (concat "foo" "bar") // ^^^ ^^^ 字符串標記 // // 我們先檢查左雙引號: if (char === """) { // 創建一個value變量用于保存字符串. let value = ""; // 我們將忽略雙引號,因為我們關心的是雙引號包裹的文本. char = input[++current]; // 然后我們遍歷后面的字符串,直到我們遇到右雙引號 while (char !== """) { value += char; char = input[++current]; } // 忽略右雙引號,同理,因為我們關心的是雙引號包裹的文本. char = input[++current]; // 創建類型為string的標記,并放進tockens數組 tokens.push({ type: "string", value }); continue; } // 最后一種類型的標記是name標記,這是一串字符而不是數字,也就是lisp語法中的函數名 // // (add 2 4) // ^^^ // name 標記 // let LETTERS = /[a-z]/i; if (LETTERS.test(char)) { let value = ""; // 同理,我們遍歷,并將它們拼接起來 while (LETTERS.test(char)) { value += char; char = input[++current]; } // 并且創建一個類型為name的標記,存儲于tokens數組 tokens.push({ type: "name", value }); continue; } // 最后,如果我們到這里還沒有匹配一個字符, 我們將拋出一個錯誤然后退出 throw new TypeError("I dont know what this character is: " + char); } // 在tokenizer函數的末尾我們將tokens數組返回 return tokens; }
舉個例子,對于(add 123 456)這段lisp語言代碼,tokenizer化之后得到的結果如下:
5.1.2、 句法解析之parser實現句法解析的目標就是將tokens數組轉換成AST。也就是下面的過程:
[{ type: "paren", value: "(" }, ...] => { type: "Program", body: [...] }
所以,我們定義一個parse函數,接收我們的tokens數組作為參數:
function parser(tokens) { // 同樣我們維持一個current變量用作游標 let current = 0; // 但是這次我們使用遞歸而不是while循環,所以我們定義了walk函數 function walk() { // 在walk函數內部,我們首先拿到tokens數組中current索引處存放的標記 let token = tokens[current]; // 我們將把每種類型的標記以另外一種結構關系存儲,以體現句法關系 // 首先從數字token開始 // // 我們檢查看有沒有數字token if (token.type === "number") { // 如果有,我們移動游標 current++; // 并且我們會返回一個叫做“NumberLiteral”的新的AST節點并且將它的value屬性設置為我們標記對象的value屬性 return { type: "NumberLiteral", value: token.value, }; } // 如果我們有string類型的標記,我們會和數字類型類似,創建一個叫做“StringLiteral”的AST節點 if (token.type === "string") { //同樣移動游標 current++; return { type: "StringLiteral", value: token.value, }; } // 接下來我們查找CallExpressions. 我們是通過左括弧來開始這個過程的 if ( token.type === "paren" && token.value === "(" ) { // 我們將忽略左括弧,因為在AST里面,AST就是有句法關系的,所以我們不關心左括弧本身了 token = tokens[++current]; // 我們創建一個叫做CallExpression的基礎節點,并且將節點的名字設置為當前標記的value屬性, // 因為左括弧標記的下一個標記就是函數名字 let node = { type: "CallExpression", name: token.value, params: [], }; // 我們移動游標,忽略掉name標記,因為函數名已經存起在CallExpression中了 token = tokens[++current]; // 然后現在我們遍歷每一個標記,找到CallExpression的參數直至遇到右括弧 // // 現在,這里就是遞歸出場的地方了,為了避免陷入無限的嵌套節點解析,我們采用遞歸的方式來搞定這個事情 // // 為了更好的解釋這個東西,我們以我們的Lisp代碼舉例,你可以看到,add的參數是一個數字以及一個嵌套的CallExpression, // 這個嵌套的函數調用包含它自己的數字參數 // // (add 2 (subtract 4 2)) // // 你特可以從它的tokens數組中發現它有很多右括弧 // // [ // { 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: ")" }, <<< 右括弧 // ] // // 我們將依賴于嵌套的walk函數來增加我們的游標 // 所以我們創建一個while循環,這個while循環將一直進行直到遇到一個類型是paren的標記并且這個標記的值是一個右括弧 while ( (token.type !== "paren") || (token.type === "paren" && token.value !== ")") ) { // 我們將調用walk函數,這個函數將返回一個節點, 我們將把這個返回的節點放到當前節點的params // 數組中存儲起來,這樣嵌套關系再AST里面就體現出來了 node.params.push(walk()); token = tokens[current]; } // 最后,我們需要最后一次移動游標用于忽略右括弧 current++; // 并且返回節點 return node; } // 同樣,如果我們沒有識別出標記的類型,我們也會拋出一個錯誤 throw new TypeError(token.type); } // 現在walk函數已經定義好了, 我們需要定義我們的AST樹了,這個AST樹有一個“Program”根節點: let ast = { type: "Program", body: [], }; // 然后我們要啟動我們的walk函數, 將AST節點放入根節點的body數組里面 // // 我們在循環里面做這個是因為,我們可能會遇到連著的多個函數調用,比如說像這樣的: // // (add 2 2) // (subtract 4 2) //啟動walk while (current < tokens.length) { ast.body.push(walk()); } // 在解析函數的最后,我們將返回生成的AST. return ast; }
任然以前面的例子舉例,我們解析后得到的AST如下:
5.2、 轉換的實現現在我們已經有了我們的AST,我們想要一個訪問者可以訪問不同的節點,無論何時匹配到對應的節點類型的時候,我們都可以調用訪問者上的方法。
所以我們定義一個旅行者函數,這個函數接收兩個參數,第一個參數為AST樹,第二個參數是一個訪問者。這個訪問者需要實現不同類型的AST節點需要調用的一些方法:
traverse(ast, { Program: { enter(node, parent) { // ... }, exit(node, parent) { // ... }, }, CallExpression: { enter(node, parent) { // ... }, exit(node, parent) { // ... }, }, NumberLiteral: { enter(node, parent) { // ... }, exit(node, parent) { // ... }, }, });
因此,我們的旅行者函數的實現如下,它接收AST和一個訪問者作為參數,并且在里面還定義了兩個方法:
function traverser(ast, visitor) { // 定義一個traverseArray函數,可以是我們迭代一個數組,然后調用我們稍后定義的traverseNode函數 function traverseArray(array, parent) { array.forEach(child => { traverseNode(child, parent); }); } // traverseNode函數接收一個AST節點以及它的父節點,所以它也可以傳遞給我們的訪問者函數 function traverseNode(node, parent) { // 我們首先檢查訪問者匹配類型的方法 let methods = visitor[node.type]; // 如果該AST節點類型存在enter方法,我們將以當前node及其父節點作為參數調用該方法 if (methods && methods.enter) { methods.enter(node, parent); } // 接下來我們會根據節點類型來把事情劃分開來 switch (node.type) { // 首先我們從頂級節點Program開始,由于該頂級節點有一個叫做body的屬性,這個屬性中是一個AST節點組成的數組 // 我們將調用traverseArray函數來遞歸它 // // (記住traverseArray函數會反過來調用traverseNode函數,所以我們讓這個AST被遞歸的訪問) case "Program": traverseArray(node.body, node); break; // 接下來我們對CallExpression節點做同樣的事情,并且訪問它們的參數 case "CallExpression": traverseArray(node.params, node); break; // 對于數字節點以及字符串節點,他們沒有任何的子節點,所以我們直接break. case "NumberLiteral": case "StringLiteral": break; // 并且再一次,如果沒有識別出對應的節點類型,就拋出錯誤 default: throw new TypeError(node.type); } // 如果訪問者上有exit方法,我們將以該節點和它的父節點作為參數調用exit方法 if (methods && methods.exit) { methods.exit(node, parent); } } // 最后,我們啟動traverser,這是通過調用traverseNode實現的,并且traverseNode第二個參數是null,因為定級節點本身就沒有父節點. traverseNode(ast, null); }
前面我們已經寫好了traverser函數,而traverser函數對節點的主要操作都是通過它的第二個參數,也就是訪問者來完成的,在上面,我們并沒有定義訪問者的具體實現,只是定義了enter和exit兩個接口,實際上這兩個接口所做的事情就是轉換步鄹真正干的事情。為此我們定義transformer函數。
transformer函數接收AST,將它傳遞給traverser函數,并且transformer函數內部還為traverser函數提供訪問者。最終transformer函數返回一個新建的AST。
比如以前面那個例子為例,得到的AST和轉換后的AST如下:
---------------------------------------------------------------------------- Original AST | Transformed AST ---------------------------------------------------------------------------- { | { type: "Program", | type: "Program", body: [{ | body: [{ type: "CallExpression", | type: "ExpressionStatement", name: "add", | expression: { params: [{ | type: "CallExpression", type: "NumberLiteral", | callee: { value: "2" | type: "Identifier", }, { | name: "add" type: "CallExpression", | }, name: "subtract", | arguments: [{ params: [{ | type: "NumberLiteral", type: "NumberLiteral", | value: "2" value: "4" | }, { }, { | type: "CallExpression", type: "NumberLiteral", | callee: { value: "2" | type: "Identifier", }] | name: "subtract" }] | }, }] | arguments: [{ } | type: "NumberLiteral", | value: "4" -------------------------------- | }, { | type: "NumberLiteral", | value: "2" | }] | } | } | }] | } ----------------------------------------------------------------------------
所以我們的transformer函數的具體實現如下:
function transformer(ast) { // 我們將創建一個新的AST(即newAst),它和我們原來的AST類似,有一個Program根節點 let newAst = { type: "Program", body: [], }; // 接下來,我們會做一些取巧的操作,我們在父節點上定義一個\_context屬性, // 我們會將節點放入父節點的\_context屬性中 // 通常你會有更好的抽象(也許會復雜些),但是在這里我們這樣做使得事情變得相對簡單 // // 你僅僅需要記住的是,context是一個從老AST到新AST的引用 ast._context = newAst.body; // 我們以老ast和一個訪問者作為參數調用traverser函數 traverser(ast, { // 第一個訪問者的屬性是用來處理NumberLiteral的 NumberLiteral: { // 在enter方法中會對節點進行訪問. enter(node, parent) { // 在這里面我們會創建一個新的AST節點,這個節點任然以NumberLiteral命名 // 我們會將這個節點放入該節點父親的\_context屬性中 parent._context.push({ type: "NumberLiteral", value: node.value, }); }, }, // 接下來是StringLiteral StringLiteral: { enter(node, parent) { parent._context.push({ type: "StringLiteral", value: node.value, }); }, }, // 接下來是CallExpression CallExpression: { enter(node, parent) { // 我們創建一個新的節點CallExpression,它有一個嵌套的標識符 let expression = { type: "CallExpression", callee: { type: "Identifier", name: node.name, }, arguments: [], }; // 接下來,我們在原始的CallExpression節點上定義一個新的context用于引用 // expression變量上的arguments屬性 // 這樣我們可以加入參數 node._context = expression.arguments; // 接著我們檢查父節點是不是一個CallExpression節點 // 如果不是 if (parent.type !== "CallExpression") { // 我們將用一個ExpressionStatement節點包裹這個CallExpression節點 // 這么做是因為頂級CallExpression節點實際上就是statement // 也就是說,如果某個CallExpression節點的父節點不是CallExpression節點 // 那么這個CallExpression節點應該就是函數聲明 expression = { type: "ExpressionStatement", expression: expression, }; } // 最后我們將這個新的CallExpression(可能被ExpressionStatement包裹) // 放入parent._context parent._context.push(expression); }, } }); // 在transformer函數的最后,我們把我們剛創建的新AST返回 return newAst; }
我們同樣以前面的例子來看一下新創建AST長什么樣子:
5.3、 代碼生成的實現現在讓我們進入我們的最后一個步鄹:代碼生成。我們的代碼生成函數會遞歸的調用自己用來打印它的節點到一個很大的字符串。也就是完成由newAST到代碼的過程:
newAst => generator => output
function codeGenerator(node) { // 我們會根據節點的type類型來將事情分別處理 switch (node.type) { // 如果我們有一個Program節點,我們將遍歷body中的每一個節點并且對每一個節點遞調用codeGenerator // 函數,并且將它們的結果用一個換行符連接起來 case "Program": return node.body.map(codeGenerator) .join(" "); // 對于ExpressionStatement節點,我們將在節點的expression節點上調用 // codeGenerator函數,然后我們會加上一個分號(即;) case "ExpressionStatement": return ( codeGenerator(node.expression) + ";" // << (...because we like to code the *correct* way) ); // 對于CallExpression節點,我們會打印callee并開始一個做括弧 // 我們會遍歷該節點的arguments屬性,然后對每個屬性調用codeGenerator方法, // 將他們的結果用逗號分隔,最后在后面加一個右括弧 case "CallExpression": return ( codeGenerator(node.callee) + "(" + node.arguments.map(codeGenerator) .join(", ") + ")" ); // 對于標識符,我們將返回節點的名字 case "Identifier": return node.name; // 對于NumberLiteral節點,我們返回它的value屬性 case "NumberLiteral": return node.value; // 對于StringLiteral節點,我們用引號將它的value屬性值包裹起來 case "StringLiteral": return """ + node.value + """; // 如果沒有識別節點,我們將拋出錯誤 default: throw new TypeError(node.type); } }
同樣以上面例子舉例,它的輸出結果如圖:
6、編譯器(compiler)的實現現在,編譯器的三大步鄹的代碼都已經實現了,我們現在開始實現編譯器,它的方式就是將三個步鄹鏈接起來,可以將這幾個步鄹描述如下:
1. input => tokenizer => tokens 2. tokens => parser => ast 3. ast => transformer => newAst 4. newAst => generator => output
我們的編譯器代碼如下:
function compiler(input) { let tokens = tokenizer(input); let ast = parser(tokens); let newAst = transformer(ast); let output = codeGenerator(newAst); // and simply return the output! return output; }
最后作為一個模塊,我們希望別人去使用它,因為我們的每個函數都是相對獨立的一個功能模塊,所以我們將這里面的每個函數都導出:
module.exports = { tokenizer, parser, traverser, transformer, codeGenerator, compiler, };
更多相關和無關內容歡迎瀏覽Github和個人博客
書把手系列還包括:手把手教你實現一個簡單的Promise,手把手教你實現一個簡單的MVC模式,手把手教你實現一個簡單的MVP模式,手把手教你實現一個簡單的MVVM模式。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/92628.html
摘要:先看效果演示接下來手把手教你實現這樣的效果。的核心功能都在中實現,如果要進行二次開發直接引用即可。在及以上版本中默認是隱藏的。首次調試,手機會彈出是否允許某臺電腦以方式調試該手機的問詢對話框,勾選允許使用這臺計算機進行調試。先看效果演示?接下來手把手教你實現這樣的效果。?minicap簡介??? minicap是一個可以遠程獲取android屏幕畫面的開源庫,它在低版本的Android系統上...
摘要:上集回顧從零開始手把手教你實現一個一上一集我們介紹了什么是,為什么要用,以及我們要怎樣來實現一個。完成后,在命令行中輸入安裝下依賴。最后返回這個目標節點。明天,我們迎接挑戰,開始處理數據變動引起的重新渲染,我們要如何新舊,生成補丁,修改。 上集回顧 從零開始手把手教你實現一個Virtual DOM(一)上一集我們介紹了什么是VDOM,為什么要用VDOM,以及我們要怎樣來實現一個VDOM...
摘要:本系列是一個教程,下面貼下目錄手把手教你從零寫一個簡單的手把手教你從零寫一個簡單的模板篇今天給大家帶來的是實現一個簡單的類似一樣的前端框架,框架現在應該算是非常主流的前端數據驅動框架,今天我們來從零開始寫一個非常簡單的框架,主要是讓大家 本系列是一個教程,下面貼下目錄~1.手把手教你從零寫一個簡單的 VUE2.手把手教你從零寫一個簡單的 VUE--模板篇 今天給大家帶來的是實現一個簡單...
摘要:下面是我的組件庫大致的目錄結構如下整個組件庫的出口在,里面的內容差不多是下面這樣的我的代碼庫的為。改成下面這樣我們給傳了一個參數,表示需要處理的,表示組件在組件庫內部的路徑。要完成一個高質量的,還有很多的工作要做。 需求 在最近的開發過程中,不同的項目、不同的頁面都需要用到某種UI控件,于是很自然的將這些UI控件拆出來,單獨建立了一個代碼庫進行維護。下面是我的組件庫大致的目錄結構如下:...
閱讀 1767·2023-04-26 01:41
閱讀 3073·2021-11-23 09:51
閱讀 2733·2021-10-09 09:43
閱讀 9019·2021-09-22 15:13
閱讀 2452·2021-09-07 09:59
閱讀 2624·2019-08-30 15:44
閱讀 1132·2019-08-30 12:45
閱讀 2616·2019-08-30 12:43