<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
本文內容基於倉庫原始碼進行學習
最近在研究一些構建側的東西,在翻 babel 官網的時候看到了這樣一段話:
For an awesome tutorial on compilers, check out the-super-tiny-compiler, which also explains how Babel itself works on a high level.
出於學習的心態,去翻了一下這個倉庫原始碼,以下是筆者的一些學習的記錄,希望看完之後能對你理解 babel 的工作原理有一些幫助~
the-super-tiny-compiler
是一個程式碼行數只有不到 200 行的超級小型的 compiler,但通過這個 compiler 能學習到最基礎的 compile 原理,包括 babel 也是基於這樣的原理來進行開發的。
倉庫本身的例子是將一組 lisp 風格的函數語法編譯成了 C 風格的函數語法,舉例子來說:
LISP 風格 | C 風格 | |
---|---|---|
2+2 | (add 2 2) | add(2,2) |
4-2 | (subtract 4 2) | substract(4, 2) |
2 + (4 - 2) | (add 2 (substract 4 2)) | add(2, (substract(4, 2))) |
這就大概是這次 compiler 需要完成的事情,可能看上去語法不是很完整,但是也能夠演示現代編譯器的主要部分思想了。
大多數的 Compilers 都會把編譯過程分成三個主要的過程: parse、transform 以及 code generate:
parse 主要分為兩個步驟: 詞法分析以及語法分析。
例如前面提到的 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: ')' } ]
語法分析處理出來的 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', } ] }] }] }
transform 主要就是拿到 parse 得到的抽象語法樹,並基於此做出一些修改。tranform 這個過程既可以基於當前語言的風格去修改 ast 也可以使用一種新的語言風格。
下面基於前面的 ast 結構來展示 transform 這個過程的工作流程。
可以看到,ast 裡面的元素看起來都很相似,這些元素組成了 ast 的子結點,這些子結點的資料結構型別描述了程式碼中的一個單獨的部分(例如變數、宣告語句,表示式等等)。
例如上面提到的型別是 NumberLiteral
的節點:
{ type: 'NumberLiteral', value: '2', }
或者更復雜一點的子節點型別:
{ type: 'CallExpression', name: 'substract', params: [...nested nodes here ...], }
在 transfrom 這個過程中,我們可以通過增/刪/改來操作抽象語法樹結點,或者可以直接基於當前的抽象語法樹建立一個新的出來。
既然這裡我們的目標是將輸入的程式碼轉換成一種新的語言風格的程式碼(Lisp -> C),所以這裡會建立一個針對新語言的全新 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,可能就需要介紹所有的種類的抽象。但目前來看,存取所有結點的方法已經足夠了。
存取(visiting) 這個詞代表一種在物件結構內對元素進行操作的模式。
這裡我們可以建立一個 visitor 物件,這個物件包括一些方法用於接收不同的結點。
例如:
var visitor = { NumberLiteral() {}, CallExpression() {} };
因此當我們遍歷 ast 的時候,如果匹配到了對應 type 的結點,可以呼叫 visitor 中的方法來處理。
Compiler 的最後一個階段就是 generate, 這個階段做的事情可能會和 transformation 重疊,但是程式碼生成最主要的部分還是根據 AST 來輸出程式碼。
Generate 有幾種不同的工作方式,有些 Compilers 會重用之前生成的 token,有些則會建立獨立的程式碼錶示,以便於線性輸出程式碼,但接下來我們還是著重於使用之前生成好的 AST。
我們的生成器需要知道如何列印 AST 中的所有型別結點,然後遞迴呼叫自身,知道所有的程式碼都被列印到一個很長的字串中。
以上就是 Compiler 所有的部分了,但並不是所有的 Compiler 都是這樣,不同的 compiler 目的不同,所以也可能需要不同的步驟。
接下來就開始程式碼的編寫:
按照前面的理論分析,我們一步先進行 parser 這個階段裡面的詞法分析器(tokenizer)。
這個函數接收一個字串,然後將其分割成由 token 組成的陣列:
ex:
(add 2 (substract 4 2))
=> [{ type: 'paren', value: '('}, ...]
因此可以編寫這樣的一個函數:
const tokenizer = (input) => { const tokens = []; let current = 0; while (current < input.length) { let char = input[current]; if (char === '(') { tokens.push({ type: 'paren', value: '(', }) current++; continue; } if (char === ')') { tokens.push({ type: 'paren', value: ')', }) current ++; continue; } // 空格目前不需要處理 if (/s/.test(char)) { current++; continue; } // 處理數位 if (/[0-9]/.test(char)) { let value = ''; // 一直遍歷直到遇到非數位的字元 while (/[0-9]/.test(char)) { value += char; char = input[++current]; } tokens.push({ type: 'number', value, }) continue; } // 處理字串 if(/[a-z]/i.test(char)) { let value = ''; while(/[a-z]/i.test(char)) { value += char; char = input[++current]; } tokens.push({ type: 'name', value, }); continue; } // 如果存在匹配不上的 token 就拋錯 throw new Error(`Unknown token: ${char}`); } return tokens; }
詞法分析器接收語法分析得到的 token 陣列,然後將其轉換成 AST 結構。
例如:
[{ type: 'paren', value: '(' }, ...]
=> { type: 'Program', body: [...] }
const parser = (tokens) => { let current = 0; const walk = () => { const token = tokens[current]; // 如果是 number 型別的結點,返回一個新的 ast 節點即可 if (token.type === 'number') { current++; return { type: 'NumberLiteral', value: token.value, } } // 接下來檢查 CallExpression 型別,先從左圓括號開始 if ( token.type === 'paren' && token.value === '(' ) { // 跳過左圓括號 token = tokens[++current]; // 首先會建立一個 CallExpression 的根節點,然後 name 設定成當前的 token.value // 因為左圓括號後的 token 一定是函數名稱 const node = { type: 'CallExpression', name: token.value, params: [], }; // 這裡再跳一次函數名稱 token = tokens[++current]; // 通過迴圈遍歷接下來的每個 token,直到遇到右圓括號 // 這些 token 會成為 `CallExpression` 的 `params` // 以 lisp 風格的程式碼來說: (add 2 (substract 4 2)) /** * token 中會有很多圓括號 * [ { 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: ')' } <<< 右圓括號 ] 遇到巢狀的 CallExpressions 的時候,會通過 walk 函數來處理 * */ while ( token.type !== 'paren' || (token.value !== ')' && token.type === 'paren') ) { node.params.push(walk()); token = tokens[current]; } current++; return node; } throw new Error(`Unknown token type: ${token.type}`); } const ast = { type: 'Program', body: [], } /** * 使用遞迴函數將結點處理進 ast.body 中 */ while (current < tokens.length) { ast.body.push(walk()); } return ast; }
通過語法分析得到 ast 之後,接下來需要一個遍歷器 (visitors) 去遍歷結點。然後當遇到某個型別的結點的時候,可以呼叫 visitors 中對應的型別處理常式:
traverse(ast, { Program(node, parent) { // ... }, CallExpression(node, parent) { // ... }, NumberLiteral(node, parent) { // ... }, });
因此我們的程式碼可以這樣寫:
const traverser = (ast, visitor) => { // 用於對陣列中的每個元素都呼叫 traverseNode 函數 const traverseArray = (array, parent) => { array.forEach((child) => { traverseNode(child, parent); }); } // 函數接收一個 node 以及其父結點作為引數 // 這個結點會被傳入到 visitor 中相應的處理常式那裡 const traverseNode = (node, parent) => { const method = visitor[node.type]; if (method) { method(node, parent); } // 對不同的結點分開處理 switch (node.type) { case 'Program': traverseArray(node.body, node); break; case 'CallExpression': traverseArray(node.params, node); break; // 這種情況下就沒有子節點了,直接 break 出去 case 'NumberLiteral': break; default: throw new Error(`Unknown node type: ${node.type}`); } } traverseNode(ast, null); }
轉換器配合上面的遍歷器來一起使用,它接收之前構建好的 ast,然後將其和 visitor 一起傳入遍歷器中,從而得到一個全新的 AST 出來。
原始的 AST 結構為( add 2 (subtract 4 2)
):
{ 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 結構為( add(2, subtract(4, 2))
):
{ 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', }] }] } } }
接下來我們可以這樣編寫對應的轉換器程式碼:
const transformer = (ast) => { const newAst = { type: 'Program', body: [], } ast._context = newAst.body; traverser(ast, { // 處理 NumberLiteral 型別 NumberLiteral: (node, parent) => { parent._context.push({ type: 'NumberLiteral', value: node.value, }); }, // 處理 CallExpression 型別 CallExpression: (node, parent) => { // 初始化一個 CallExpression 的新節點 // 裡面放個巢狀的 Identifier 節點 const expression = { type: 'CallExpression', callee: { type: 'Identifier', name: node.name }, arguments: [], }; node._context = expression.arguments; // 看看父節點是不是 CallExpression if (parent.type !== 'CallExpression') { // 如果不是的話,就將 CallExpression 節點包在一個叫 `ExpressionStatement` 的語句節點裡 // 這麼做是因為 top level 的 CallExpression 在 JavaScript 中也可以被當成是宣告語句 expression = { type: 'ExpressionStatement', expression, }; } // 最後我們把 CallExpression 放入父結點的 context 中 parent._context.push(expression); } }); return newAst; }
程式碼生成器同樣是個遞迴函數,最後會將 AST 中的每個結點列印到一個大的字串中:
const codeGenerator = (node) => { switch (node.type) { // 如果是 Program,則會遍歷 'body' 屬性中的每個結點 // 並且對這些結點進行遞迴 codeGenerator,再把結果列印進新的一行裡面 case 'Program': return node.body.map(codeGenerator).join('n'); // 對於 ExpressionStatement 對 expression 屬性進行遞迴呼叫,並加個分號 case 'ExpressionStatement': return `${codeGenerator(node.expression)};`; // 對於 CallExpression 對 callee 屬性進行遞迴呼叫,接著加上(括號 // 然後對 arguments 屬性進行遞迴呼叫,並加上)括號 case 'CallExpression': return `${codeGenerator(node.callee)}(${node.arguments.map(codeGenerator).join(', ')})`; // 對於 Identifier,直接返回 name case 'Identifier': return node.name; // 對於 NumberLiteral,直接返回 value case 'NumberLiteral': return node.value; default: throw new Error(`Unknown node type: ${node.type}`); } }
到這一步,基本上所有的流程就已經完成了,我們可以建立一個 compiler 函數,通過呼叫上面的函數就可以完成整個 compiler 的工作了:
程式碼只需要以下簡單幾步即可:
const compiler = (input) => { const tokens = tokenizer(input); const ast = parser(tokens); const newAst = transformer(ast); return codeGenerator(newAst); }
我們可以輸入前面的幾組測試例子,能保證得到的結果是正確的。
至此一個 tiny-compiler 就被造出來了,babel 的編譯流程也是基於此來完成,因為 babel 本身是個 source to source 的 compiler,整個流程也是分為 parser -> transform -> code generate 這三個步驟完成,具體可以參考
以上就是基於JS實現一個小型編譯器的詳細內容,更多關於JS編譯器的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45