用JavaScript實(shí)現(xiàn)一個編譯器
現(xiàn)在前端開發(fā)中,我們常常會用到babel來編譯例如react、vue框架的代碼,以支持更多的(更古老的)瀏覽器,babel編譯代碼的過程就是編譯原理的應(yīng)用之一。
Babel is a JavaScript compiler!這是Babel官方對于babel的定義。身為前端工程師,因此有必要了解編譯原理,幸運(yùn)的是,“The Super Tiny Compiler”開源項(xiàng)目利用JavaScript寫了一個簡單的編譯器。
麻雀雖小,五臟俱全,通過對該項(xiàng)目的學(xué)習(xí),一起加深對編譯過程的理解,以提升我們寫出更高質(zhì)量的程序!
一、收益
通過掌握(了解)編譯原理,將有如下收益:
加深對編程語言的認(rèn)識,無論何種編程語言,萬變不離其宗
殊途同歸,有利于理解babel等轉(zhuǎn)譯器、eslint、prettier、less等工具的工作原理,可開發(fā)相關(guān)插件
可以造更多輪子了🐶
二、編譯過程概述
編譯過程的具體實(shí)現(xiàn)主要分為三步驟:
- 代碼解析(parse)
 - 代碼轉(zhuǎn)換(Transformation)
 - 代碼生成(Code Generation)
 
通過上述三步驟,可以將我們的原代碼,轉(zhuǎn)換(編譯)到目標(biāo)代碼,例如把javascript代碼轉(zhuǎn)換到python一樣。
編譯過程
“The Super Tiny Compiler”項(xiàng)目中是將LISP語言編譯為C語言,如下案例:
- * LISP(source) C(target)
 - *
 - * 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))
 
二、轉(zhuǎn)換過程
基于“The Super Tiny Compiler”項(xiàng)目,實(shí)現(xiàn)一個將LISP函數(shù)轉(zhuǎn)換到C語言函數(shù)編寫形式!
- (源代碼)LISP CODE: (add 2 (subtract 4 2))
 - (目標(biāo)代碼)C CODE: add(2, subtract(4, 2))
 
2.1 解析(Parse)
具象到具象的轉(zhuǎn)換是很難的,但抽象到具象的轉(zhuǎn)換往往是容易的。
解析的過程中包含了兩個關(guān)鍵步驟詞法分析(Lexical Analysis)和語法分析(Syntactic Analysis),解析就是一個具象到抽象的過程。
2.1.1 詞法分析
詞法分析的過程,主要是將原代碼(字符串),通過分詞的方式生成一個具有描述程序語義的token列表。
分詞的原理:逐個讀取源代碼中的字符,與預(yù)設(shè)的關(guān)鍵詞、字符串、數(shù)字、操作符等LISP語言定義的語法相關(guān)規(guī)則,轉(zhuǎn)換成 {type: 'xx', value: 'xx'} 的具有描述意義的形式
例如LISP:(add 2 (subtract 4 2)),經(jīng)過詞法分析會得到如下結(jié)果:
- [
 - { 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: ')' },
 - ]
 
這樣一個列表(暫稱作:tokens列表)按照順序下來很好的描述了源代碼中的字符串和編程語義。
2.1.2 語法分析
詞法分析后得到的tokens列表已經(jīng)可以描述LISP的語法,但是還并不抽象,因?yàn)橹庇^看來,我們無法解讀這個程序的意思,這就需要將其轉(zhuǎn)換為AST(Abstract Syntax Tree,抽象語法樹)。
為什么要將其轉(zhuǎn)換到AST,AST能更好的描述源代碼的語義、描述結(jié)構(gòu)更加通用,tokens列表只是描述了“符號”的意義,可以將詞法分析過程看作是分類過程,而語法分析的過程,則是將符號組合,使其具有了執(zhí)行順序以及執(zhí)行規(guī)則的語法,抽象語法樹,因?yàn)楦橄螅蚨芨咝兽D(zhuǎn)換到其他形式。
操作LISP得到的AST能更好轉(zhuǎn)換到C語言的AST,因?yàn)樗麄兊腁ST結(jié)構(gòu)都是類似的,操作AST比tokens更容易。
語法分析的過程,將tokens列表轉(zhuǎn)化成為一個樹結(jié)構(gòu):
- {
 - 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',
 - }
 - ]
 - }
 - ]
 - }
 - ]
 - }
 
2.2 代碼轉(zhuǎn)換(Transform)
代碼轉(zhuǎn)換的過程是將傳入的AST結(jié)構(gòu),通過在AST上例如增、刪、改屬性,將傳入AST轉(zhuǎn)換為C語言需要的標(biāo)準(zhǔn)AST結(jié)構(gòu)。
為了實(shí)現(xiàn)轉(zhuǎn)換,我們增加了一個 traverser(ast, visitor) 函數(shù),這個函數(shù)接收parse過程得到的AST和visitor規(guī)則轉(zhuǎn)換對象。
visitor對象實(shí)際可理解為轉(zhuǎn)換規(guī)則,traverser函數(shù)在遍歷AST結(jié)構(gòu)時,會根據(jù)visitor中定義的規(guī)則執(zhí)行轉(zhuǎn)換,用于生成新的符合C語言描述標(biāo)準(zhǔn)的AST結(jié)構(gòu)。
最后通過 transform(ast)(預(yù)處理,定義visitor對象) -> traverser(ast, visitor) 過程,得到一個新的AST結(jié)構(gòu):
- {
 - 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'
 - }
 - ]
 - }
 - }
 - }
 - ]
 - }
 
2.3 生成目標(biāo)代碼(Code Generate)
最后通過解析符合C語言標(biāo)準(zhǔn)的AST,根據(jù)C語言的語法規(guī)則,轉(zhuǎn)換成為C語言格式
codeGenerator(newAst) 函數(shù)如下,接收新生成的AST結(jié)構(gòu):
- function codeGenerator(newAst) {
 - switch (newAst.type) {
 - case "Program":
 - return newAst.body.map(codeGenerator).join("\n");
 - case "ExpressionStatement":
 - return codeGenerator(newAst.expression) + ";";
 - case "CallExpression":
 - return (
 - codeGenerator(newAst.callee) +
 - "(" +
 - newAst.arguments.map(codeGenerator).join(", ") +
 - ")"
 - );
 - case "Identifier":
 - return newAst.name;
 - case "NumberLiteral":
 - return newAst.value;
 - case "StringLiteral":
 - return '"' + newAst.value + '"';
 - default:
 - throw new TypeError(newAst.type);
 - }
 - }
 
同時還做了代碼的部分格式化,比如空格、換行符添加等。
此時自然會思考下,VScode編輯器中的Prettier代碼格式化插件是不是也是這么做的?
四、總結(jié)
推薦大家完整閱讀一下“The Super Tiny Compiler”開源項(xiàng)目,作者寫的代碼注釋也非常詳細(xì)。編譯(轉(zhuǎn)譯)的過程原理基本類似,還有很多優(yōu)秀的項(xiàng)目,比如codeMirror、babel、esprima、acorn、recast都是值得閱讀的源碼,喜歡的小伙伴一定要去瞅瞅。
Reference
- 《Babel Docs》- https://babeljs.io/docs/en/
 - 《the super tiny complier》- https://github.com/jamiebuilds/the-super-tiny-compiler/
 

















 
 
 





 
 
 
 