後續內容更新,請前往:我的博客,歡迎一塊兒交流。html
源文件:the-super-tiny-compiler
詳細中文註釋:the-super-tiny-compiler前端
稍微接觸一點前端,咱們都知道如今前端「ES6即正義」,然而瀏覽器的支持還處於進行階段,因此咱們經常會用一個神奇的工具將 ES6 語法轉換爲目前支持比較普遍的 ES5 語法,這裏咱們所說的神奇的工具就是編譯器。編譯器功能很是純粹,將字符串形式的輸入語言編譯成目標語言的代碼字符串(以及sourcemap),經常使用的編譯器除了咱們熟知的 Babel 以外,還有 gcc。不過咱們今天的主角是號稱多是有史以來最小的編譯器the-super-tiny-compiler,去掉註釋也就200多行代碼,做者 James Kyle 更是 Babel 的活躍維護者之一。這個編譯器的功能很簡單,主要把 Lisp 風格的函數調用轉換成 C 風格的,例如:node
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)) |
絕大多數編譯器的編譯過程都差很少,主要分爲三個階段:
解析:將代碼字符串解析成抽象語法樹。
轉換:對抽象語法樹進行轉換操做。
代碼生成:根據轉換後的抽象語法樹生成目標代碼字符串。git
解析過程主要分爲兩部分:詞法分析和語法分析。
一、詞法分析是由詞法分析器把原始代碼字符串轉換成一系列詞法單元(token),詞法單元是一個數組,由一系列描述獨立語法的對象組成,它們能夠是數值、標籤、標點符號、運算符、括號等。
二、語法分析是由語法分析器將詞法分析器生成的詞法單元轉化爲可以描述語法結構(包括語法成分及其關係)的中間表示形式(Intermediate Representation)或抽象語法樹(Abstract Syntax Tree),其中抽象語法樹(簡稱AST)是個深層嵌套的對象。github
咱們簡單看一下 the-super-tiny-compiler 的整個解析過程:express
// 原始代碼字符串 (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', }] }] }] }
轉換過程主要任務是修改 AST,即遍歷解析過程生成的 AST,同時進行一系列操做,好比增/刪/改節點、增/刪/改屬性、建立新樹等,咱們簡單看一下 the-super-tiny-compiler 的整個轉換過程:數組
// 原始代碼字符串 (add 2 (subtract 4 2)) // 解析過程生成的 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 { type: 'Program', body: [{ type: 'ExpressionStatement', expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'add' }, arguments: [{ type: 'NumberLiteral', value: '2' }, { type: 'CallExpression', callee: { type: 'Identifier', name: 'subtract' }, arguments: [{ type: 'NumberLiteral', value: '4' }, { type: 'NumberLiteral', value: '2' }] } } }] }
根據轉換過程生成的抽象語法樹生成目標代碼字符串。瀏覽器
接下來咱們根據編譯器工做的三個階段逐一分析一下 the-super-tiny-compiler 源碼實現。函數
詞法分析器主要任務把原始代碼字符串轉換成一系列詞法單元(token)。工具
// 詞法分析器 參數:代碼字符串input function tokenizer(input) { // 當前正在處理的字符索引 let current = 0; // 詞法單元數組 let tokens = []; // 遍歷字符串,得到詞法單元數組 while (current < input.length) { let char = input[current]; // 匹配左括號 if (char === '(') { // type 爲 'paren',value 爲左圓括號的對象 tokens.push({ type: 'paren', value: '(' }); // current 自增 current++; // 結束本次循環,進入下一次循環 continue; } // 匹配右括號 if (char === ')') { tokens.push({ type: 'paren', value: ')' }); current++; continue; } // \s:匹配任何空白字符,包括空格、製表符、換頁符、換行符、垂直製表符等 let WHITESPACE = /\s/; // 跳過空白字符 if (WHITESPACE.test(char)) { current++; continue; } // [0-9]:匹配一個數字字符 let NUMBERS = /[0-9]/; // 匹配數值 if (NUMBERS.test(char)) { let value = ''; // 匹配連續數字,做爲數值 while (NUMBERS.test(char)) { value += char; char = input[++current]; } tokens.push({ type: 'number', value }); continue; } // 匹配形如"abc"的字符串 if (char === '"') { let value = ''; // 跳躍左雙引號 char = input[++current]; // 獲取兩個雙引號之間的全部字符 while (char !== '"') { value += char; char = input[++current]; } // 跳躍右雙引號 char = input[++current]; tokens.push({ type: 'string', value }); continue; } // [a-z]:匹配1個小寫字符 i 模式中的字符將同時匹配大小寫字母 let LETTERS = /[a-z]/i; // 匹配函數名,要求只含大小寫字母 if (LETTERS.test(char)) { let value = ''; // 獲取連續字符 while (LETTERS.test(char)) { value += char; char = input[++current]; } tokens.push({ type: 'name', value }); continue; } // 沒法識別的字符,拋出錯誤提示 throw new TypeError('I dont know what this character is: ' + char); } // 詞法分析器的最後返回詞法單元數組 return tokens; }
經過遍歷代碼字符串,分揀出各個詞素,而後構成由一系列描述獨立語法的對象組成的數組的詞法單元。
語法分析器主要任務是將詞法分析器生成的詞法單元轉化爲可以描述語法結構(包括語法成分及其關係)的中間表示形式(Intermediate Representation)或抽象語法樹(Abstract Syntax Tree)。
// 語法分析器 參數:詞法單元數組 function parser(tokens) { // 當前正在處理的 token 索引 let current = 0; // 遞歸遍歷(由於函數調用容許嵌套),將 token 轉成 AST 節點 function walk() { // 獲取當前 token let token = tokens[current]; // 數值 if (token.type === 'number') { // current 自增 current++; // 生成一個 AST節點 'NumberLiteral',用來表示數值字面量 return { type: 'NumberLiteral', value: token.value, }; } // 字符串 if (token.type === 'string') { current++; // 生成一個 AST節點 'StringLiteral',用來表示字符串字面量 return { type: 'StringLiteral', value: token.value, }; } // 函數 if (token.type === 'paren' && token.value === '(') { // 跳過左括號,獲取下一個 token 做爲函數名 token = tokens[++current]; let node = { type: 'CallExpression', name: token.value, params: [] }; // 再次自增 `current` 變量,獲取參數 token token = tokens[++current]; // 右括號以前的全部token都屬於參數 while ((token.type !== 'paren') || (token.type === 'paren' && token.value !== ')')) { node.params.push(walk()); token = tokens[current]; } // 跳過右括號 current++; return node; } // 沒法識別的字符,拋出錯誤提示 throw new TypeError(token.type); } // AST的根節點 let ast = { type: 'Program', body: [], }; // 填充ast.body while (current < tokens.length) { ast.body.push(walk()); } // 最後返回ast return ast; }
經過遞歸來將詞法分析器生成的詞法單元轉化爲可以描述語法結構的 ast。
// 遍歷器 function traverser(ast, visitor) { // 遍歷 AST節點數組 對數組中的每個元素調用 `traverseNode` 函數。 function traverseArray(array, parent) { array.forEach(child => { traverseNode(child, parent); }); } // 接受一個 `node` 和它的父節點 `parent` 做爲參數 function traverseNode(node, parent) { // 從 visitor 獲取對應方法的對象 let methods = visitor[node.type]; // 經過 visitor 對應方法操做當前 node if (methods && methods.enter) { methods.enter(node, parent); } switch (node.type) { // 根節點 case 'Program': traverseArray(node.body, node); break; // 函數調用 case 'CallExpression': traverseArray(node.params, node); break; // 數值和字符串,不用處理 case 'NumberLiteral': case 'StringLiteral': break; // 沒法識別的字符,拋出錯誤提示 default: throw new TypeError(node.type); } if (methods && methods.exit) { methods.exit(node, parent); } } // 開始遍歷 traverseNode(ast, null); }
經過遞歸遍歷 AST,在遍歷過程當中經過 visitor 對應方法操做當前 node,這裏和切面差很少。
// 轉化器,參數:AST function transformer(ast) { // 建立 `newAST`,它與以前的 AST 相似,Program:新AST的根節點 let newAst = { type: 'Program', body: [], }; // 經過 _context 維護新舊 AST,注意 _context 是一個引用,從舊的 AST 到新的 AST。 ast._context = newAst.body; // 經過遍歷器遍歷 參數:AST 和 visitor traverser(ast, { // 數值,直接原樣插入新AST NumberLiteral: { enter(node, parent) { parent._context.push({ type: 'NumberLiteral', value: node.value, }); }, }, // 字符串,直接原樣插入新AST StringLiteral: { enter(node, parent) { parent._context.push({ type: 'StringLiteral', value: node.value, }); }, }, // 函數調用 CallExpression: { enter(node, parent) { // 建立不一樣的AST節點 let expression = { type: 'CallExpression', callee: { type: 'Identifier', name: node.name, }, arguments: [], }; // 函數調用有子類,創建節點對應關係,供子節點使用 node._context = expression.arguments; // 頂層函數調用算是語句,包裝成特殊的AST節點 if (parent.type !== 'CallExpression') { expression = { type: 'ExpressionStatement', expression: expression, }; } parent._context.push(expression); }, } }); // 最後返回新 AST return newAst; }
這裏經過 _context 引用維護新舊 AST,簡單方便,但會污染舊AST。
// 代碼生成器 參數:新 AST function codeGenerator(node) { switch (node.type) { // 遍歷 body 屬性中的節點,且遞歸調用 codeGenerator,結果按行輸出 case 'Program': return node.body.map(codeGenerator) .join('\n'); // 表達式,處理表達式內容,並用分號結尾 case 'ExpressionStatement': return ( codeGenerator(node.expression) + ';' ); // 函數調用,添加左右括號,參數用逗號隔開 case 'CallExpression': return ( codeGenerator(node.callee) + '(' + node.arguments.map(codeGenerator) .join(', ') + ')' ); // 標識符,數值,原樣輸出 case 'Identifier': return node.name; case 'NumberLiteral': return node.value; // 字符串,用雙引號包起來再輸出 case 'StringLiteral': return '"' + node.value + '"'; // 沒法識別的字符,拋出錯誤提示 default: throw new TypeError(node.type); } }
根據轉換後的新AST生成目標代碼字符串。
function compiler(input) { let tokens = tokenizer(input); let ast = parser(tokens); let newAst = transformer(ast); let output = codeGenerator(newAst); return output; }
編譯器整個工做流程:
一、input => tokenizer => tokens
二、tokens => parser => ast
三、ast => transformer => newAst
四、newAst => generator => output將上面流程串起來,就構成了簡單的編譯器。