本篇內容主要由 the-super-tiny-compiler中的註釋翻譯而來,該項目實現了一款包含編譯器核心組成的極簡的編譯器。但願可以給想要初步瞭解編譯過程的同窗提供到一些幫助。java
add
和 subtract
他們用對應的語言分別實如餘下:內容 | 類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)) |
大部分的編譯器能夠粗略的劃分爲3個階段: 解析 Parsing,翻譯 Transformation,代碼生成Code Generationnode
解析過程一般被分爲兩個部分: 詞法分析,語法分析python
由這些詞構成的詞組用來描述語法,他們能夠是數字,文本,標點符號,運算符等等git
抽象語法樹(簡稱AST)是一個嵌套很深的對象,它以一種既容易使用又能告訴咱們不少信息的方式表示代碼。github
(add 2 (subtract 4 2))
tokens表示以下express
{ 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', }] }] }] }
得到抽象語法樹後下一個階段就是翻譯轉換。一樣,這隻須要從最後一步中提取AST並對其進行更改。它能夠用同一種語言操縱AST,也能夠將AST翻譯成一種全新的語言。數組
讓咱們看看如何轉換AST。函數
你可能會注意到咱們的AST中有看起來很是類似的元素。這些對象具備類型屬性。每一個節點都稱爲AST節點。這些節點定義了描述樹的一個獨立部分的屬性。學習
咱們有一個數字節點 "NumberLiteral"ui
{ type: 'NumberLiteral', value: '2', }
或者一個調用表達式節點
{ type: 'CallExpression', name: 'subtract', params: [...nested nodes here...], }
轉換AST時,咱們能夠經過添加/刪除/替換屬性來操縱節點,能夠添加新節點,刪除節點,也能夠不使用現有的AST直接基於它建立一個全新的AST。
因爲咱們定位的是新語言,所以咱們將專一於建立特定於目標語言的全新AST。
爲了瀏覽全部這些節點,咱們須要可以遍歷它們。 以下將經過深度優先方式的遍歷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,而不是建立單獨的AST,則可能會在這裏引入各類抽象。 可是僅訪問樹中的每一個節點就足以完成咱們要嘗試的操做。
我之因此使用「訪問」一詞,是由於存在這種模式來表示對象結構元素上的操做。
這裏的基本思想是,咱們將建立一個「訪客」對象,該對象的方法將接受不一樣的節點類型。
var visitor = { NumberLiteral() {}, CallExpression() {}, };
可是,也有可能在「退出」時調用相應的操做。 想象一下之前以列表形式的樹結構:
Program
CallExpression
當咱們往下遍歷時,咱們遍歷盡全部分支時。咱們「退出」它。 所以,沿着樹下來,咱們「進入[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)
爲了支持進入和退出操做,咱們將vistitor定義調整以下
var visitor = { NumberLiteral: { enter(node, parent) {}, exit(node, parent) {}, } };
請注意,並非說每一個編譯器看起來都和這裏描述的徹底同樣。編譯器根據目的不一樣有不少種,可能須要好比下詳細介紹的步驟更多的步驟。
如今您應該對編譯器的主要外觀有一個大體的整體瞭解。
通過上面的解釋和介紹,如今能夠開始編寫本身的編譯器了,那麼開始代碼走起。
咱們將獲取咱們的代碼串將其解析成token數組
(add 2 (subtract 4 2)) => [{ type: 'paren', value: '(' }, ...]
function tokenizer(input) { // current變量,用來標記當前讀入代碼的字符位置的遊標 let current = 0; // tokens數組變量,用來存入解析的token詞組 let tokens = []; // 開啓一個while循環,將current設置爲循環內部的增量 while(current < input.length){ // 獲取當前遊標對應的字符 let char = input[current]; // 檢查當前字符是不是一個括號 if(char=== "("){ // 若是是括號,則新增一個`paren`括號類型的,值爲作括號的詞到tokens詞組 tokens.push({ type: 'paren', value: '(', }); //而後遊標向後前進一位 current ++; // 進入下一循環 continue; } // 檢查是否右括號,如是則新增一個右括號詞組,增長遊標,繼續下一次循環 if (char === ')') { tokens.push({ type: 'paren', value:')' }) current++; continue; } // 檢查當前字符是否空格,若是是空格則直接跳過,遊標後移 // (add 123 456) // ^^^ ^^^ number let WHITESPACE = /\s/; if (WHITESPACE.test(char)) { current++; continue; } //下一個將檢測的類型是number數字.和以前不一樣的是number類型可能由多個數字字符組成,咱們須要 // 獲取整個連續的數字串做爲一個number類型的詞token let NUMBERS = /[0-9]/; if(NUMBERS.test(char)){ //新建一個value串用來設置數字字符串 let value=''; while(NUMBERS.test(char)){ value += char; char = input[++current]; } tokens.push({type:'number',value}); continue; } // 在將要實現的編譯器中也支持被雙引號括起來的字符串 // (concat "foo" "bar") // ^^^^ ^^^^ 支付串 if (char === '"') { let value = ''; char = input[++current]; while (char != '"') { value += char; char = input[++current] } // 遊標跳過終結的引號 char = input[++current]; tokens.push({ type:'string', value }) continue; } // 最後一個類型的token是`name`類型.由一串字母構成。該類型用做本編譯器 // 的lisp語法風格的函數名 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; }
function parser(tokens) { // 新建current變量做爲遊標 let current = 0; // 該方法中將用遞歸代替while循環,先定義一個walk方法 function walk() { //獲取當前token let token = tokens[current]; // 從number類型的token開始,將不一樣類型的token置入代碼的不一樣位置 if (token.type === 'number') { // 若是當前是number類型,遊標向前 current++; // 返回一個number類型的AST 節點 return { type: 'NumberLiteral', value: token.value } } // 字符token返回一個字符類型的AST節點 if (token.type === 'string') { current++; return { type: 'StringLiteral', value: token.value } } // 下面檢查是否調用表達式.先判斷是不是一個括號類型,且是左括號token if ( token.type === 'paren' && token.value === '(' ) { // 跳過當前左括號遊標,獲取下一個token token = tokens[++current]; let node = { type:'CallExpression', name: token.value, params: [] } // 遊標向前移一位跳過 name類型的token token = tokens[++current]; // 如今開始遍歷 CallExpression的參數,直到遇到右括號 // 這裏開始會存在遞歸,咱們經過遞歸解決嵌套節點問題。 // 爲了解釋這一點,讓咱們採用咱們的Lisp代碼。 您能夠看到 // add的參數是一個數字和一個包含本身的參數的嵌套的CallExpression。 // [ // { 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: ')' }, <<< Closing parenthesis // { type: 'paren', value: ')' }, <<< Closing parenthesis // ] // 咱們經過遞歸調用walk方式,去向前遍歷內嵌的`CallExpression`. // 這裏咱們建立一個While循環遍歷直到遇到左括號 while( (token.type !== 'paren')|| (token.type === 'paren' && token.value !==')')){ node.params.push(walk()); token = tokens[current]; } current++; return node; } // 若是不是以上檢測的類型則拋出異常 throw new TypeError(token.type); } let ast = { type:'Program', body:[] } while(current < tokens.length){ ast.body.push(walk()); } return ast; }
/*** * =================================== * ⌒(❀>◞౪◟<❀)⌒ * THE TRAVERSER!!! * =================================== * 如今經過parser有了一顆AST抽象語法樹,如今經過vistor訪問 * 每個節點 * traverse(ast, { * Program: { * enter(node, parent) { * // ... * }, * exit(node, parent) { * // ... * }, * }, * * CallExpression: { * enter(node, parent) { * // ... * }, * exit(node, parent) { * // ... * }, * }, * * NumberLiteral: { * enter(node, parent) { * // ... * }, * exit(node, parent) { * // ... * }, * }, * }); */ function traverser(ast, visitor) { function traverseArray(array, parent) { array.forEach(child => { traverseNode(child, parent); }); } function traverseNode(node, parent) { let methods = visitor[node.type]; 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); } /** * ============================================================================ * ⁽(◍˃̵͈̑ᴗ˂̵͈̑)⁽ * THE TRANSFORMER!!! * ============================================================================ */ /** * 下一步Ast轉化. 將已經構建好的Ast樹經過visitor轉化成一顆新的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' * | }] * (sorry the other one is longer.) | } * | } * | }] * | } * ---------------------------------------------------------------------------- */ //該方法接收類lisp抽象語法樹,轉化爲類c語言的ast樹 function transformer(ast) { //建立新的ast節點 let newAst = { type: 'Program', body: [] } // 將新ast樹的body做爲原ast樹的_context屬性 ast._context = newAst.body; traverser(ast,{ // 第一個接收數值類型的參數 NumberLiteral:{ enter(node, parent) { parent._context.push({ type: 'NumberLiteral', value:node.value }) } }, StringLiteral:{ enter(node, parent){ parent._context.push({ type:'StringLiteral', value: node.value }) } }, CallExpression:{ enter(node,parent){ let expression = { type: 'CallExpression', callee: { type: 'Identifier', name: node.name, }, arguments: [], }; // 接下來,咱們將在原CallExpression節點 //定義一個上下文,引用expression的參數,以便設置參數。 node._context = expression.arguments; // 檢測父節點是否CallExpresssion,若是不是執行下列代碼 if (parent.type !== 'CallExpression') { // 用`ExpressionStatement`節點包裹 `CallExpression` // 作這一步轉換的緣由是調用表達式最終是一個語句 expression = { type: 'ExpressionStatement', expression: expression }; } parent._context.push(expression); } } }) return newAst; }
/** * 這裏開始最後一步代碼:代碼生成 */ function codeGenerator(node) { switch (node.type) { 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); } }
/** * 最後建立`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); return output; }
如上即用javscript完成了一個簡單的編譯器,若是你習慣用其餘的語言如java,go,python等等,能夠嘗試改寫一下。固然以上介紹分享的內容只包含了編譯器的主要步驟,至關於一個編譯器的hello world,可是經過代碼實現有一個更直觀的感覺。後續有須要實現一些可能與編譯有關的功能能夠起到必定的幫助。