welcome 今天在看 babel 的时候,无意中被引到一个外链
关于编译器的优秀/简单的教程,请查看 the-super-tiny-compiler ,同时它也从宏观角度上解释了 Babel 本身是如何工作的。
觉得挺感兴趣的,加上代码也不多,就跟着思路自己理解敲了一遍。
本文主要是帮助理解编译器的原理 不做过多的其他扩展
编译器的基本组成 一般简单的编译器可以由以下几部分组成:
tokenizer 词法分析器 把代码或文本按类型分开,返回: tokens
parser 语法剖析器 对分析后的文本按照语法分析转换成 抽象语法树 返回:ast
transformer 语法转换器 把语法剖析后的抽象语法树转换成我们想要的: newAst
codeGenerator 代码生成器 把AST 转换为 目标代码
babel 初始阶段并没有做任何事,基本上等于 const babel = code=> code
; 先 tokenizer, parser 解析代码,再 transformer 的时候,完全不改动原来的 ast
接下来以最简单的编译器组成 一个环节一个环节走下去
tokenizer 词法分析器 词法分析器其实可以理解为简单的将文本切割,然后将有价值的按照相邻同等类型的 文本组合一起输出。ps:无价值指对代码生成没有影响的部分,比如js里面非文本 一个空格和一百个空格对编译器来说是没有区别的
实现思路:
有一个值存着当前的光标 current
有一个数组用来存放按类型分出来的值 tokens
对文本进行循环, 取 current
的值做 分类型处理
(
)
空格
数字
字符串
方法名
将处理后的值存进数组 token
并返回
tokenizer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 function tokenizer (input ) { let current = 0 ; const tokens = []; while (current < input.length) { let char = input[current]; if (char === "(" || char === ")" ) { tokens.push({ type : 'paren' , value : char }); current++; continue ; } const WHITESPACE = /\s/ ; if (WHITESPACE.test(char)) { current++; continue ; } const LETTERS = /[a-z]/i ; if (LETTERS.test(char)) { let name = "" ; while (LETTERS.test(char)) { name += char; current++; char = input[current]; } tokens.push({ type : 'name' , value : name }); continue ; } const NUMBERS = /[0-9]/ ; if (NUMBERS.test(char)) { let numbers = "" ; while (NUMBERS.test(char)) { numbers += char; current++; char = input[current]; } tokens.push({ type : 'number' , value : numbers }); continue ; } if (char === '"' ) { let string = '' ; current++; char = input[current]; while (char !== '"' ) { string += char; current++; char = input[current]; } tokens.push({ type : 'string' , value : string }); current++; continue ; } throw new TypeError ('不知道你输入的是什么鬼东西 ' + char); } return tokens; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const input = '(add 2 (subtract 4 2 "djwaqp"))' ;tokenizer(input);
parser 语法剖析器就是把 tokens
解析,转化为抽象语法树(AST)🌲🌲🌲,方便后续的处理。
1 [{ type : 'paren' , value : '(' }, ...] => { type : 'Program' , body : [...] }
实现思路:
首先也要有一个 current
对 tokens
进行遍历,每一项 token
进行分析处理
有一棵树,顶级结构为 {type: 'Program' , body: [...]}
根据 token.type
进行相应的归类处理:
number
: 直接返回 {type: 'NumberLiteral', value, }
string
: 直接返回 {type: 'StringLiteral', value, }
paren (
: 对下一个进行递归,直到出现 paren )
将处理后的 ast 返回
parser 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 function parser (tokens ) { let current = 0 ; const ast = { type : 'Program' , body : [] }; function walk ( ) { let token = tokens[current]; if (token.type === 'number' ) { current ++; return { type : 'NumberLiteral' , value : token.value } } if (token.type === 'string' ) { current ++; return { type : 'StringLiteral' , value : token.value } } if (token.type === 'paren' && token.value === '(' ) { token = tokens[++current]; let node = { type : "CallExpression" , name : token.value, params : [] } token = tokens[++current]; while ((token.type !== 'paren' ) || (token.type === 'paren' && token.value !== ')' )) { node.params.push(walk()); token = tokens[current]; } current++; return node; } throw new TypeError (token.type); } while (current < tokens.length) { ast.body.push(walk()); } return ast; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 input: [{ 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 : 'string' , value : 'djwaqp' }, { type : 'paren' , value : ')' }, { type : 'paren' , value : ')' }] => output: { "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" }, { "type" : "StringLiteral" , "value" : "djwaqp" }] }] }] }
transformer 顾名思义,为转换部分,最复杂 也最常用。.babelrc
添加的插件,也只是在这个环节进行操作,将原本的 ast( es6 ) 转换为目标的 newAst (es5)。
1 ast { type : 'Program' , body : [...] } => newAst { type : 'Program' , body : [...] }
实现思路:
首先,要有一颗树,和 ast 一样。 顶级结构为 { type: 'Program', body: [...] }
在 ast
上建一个引用_context
到 newAst.body
;
对 ast 树进行处理, 直接处理 _context
对 ast 上 每个类型都做 enter 处理与 exit处理的钩子
Number 或 String 直接处理并添加到树节点上即可
CallExpression 则要创建一个数组去存参数
对类型为:Program
或 CallExpression
对子级进行 递归 4
处理
将处理后的 newAst 返回
transformer 部分 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 function traverser (node, visitor ) { function traverseArray (nodeArr, parent ) { nodeArr.forEach(child => traverseNode(child, parent)); } function traverseNode (node, parent ) { const 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(node, null ); } function transformer (ast ) { const newAst = { type : 'Program' , body : [] }; 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 : [] } node._context = expression.arguments; if (parent.type !== 'CallExpression' ) { expression = { type : 'ExpressionStatement' , expression : expression } } parent._context.push(expression); } } }); return newAst; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 input: { "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" }, { "type" : "StringLiteral" , "value" : "djwaqp" }] }] }] } => output: { "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" }, { "type" : "StringLiteral" , "value" : "djwaqp" }] }] } }] }
codeGenerator 最后一步就是 codeGenerator
, 用 newAst
递归调用,根据 node
与一系列规则去生成一个 string。
1 newAst { type : 'Program' , body : [...] } => call(argumentsA, ...argumentsN);
实现思路:
根据 node.type 做对应逻辑:
Program
=> 对 node.body 进行递归
ExpressionStatement
=> 对 node.expression 进行处理
CallExpression
=> 对 node.callee 与 node.arguments 进行处理
Identifier
&& NumberLiteral
&& StringLiteral
直接返回对应的字段
codeGenerator 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 input: { "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" }, { "type" : "StringLiteral" , "value" : "djwaqp" }] }] } }] } => output: add(2 , subtract(4 , 2 , "djwaqp" ));
compiler 至此,编译器所需的几个步骤: 词法分析,解析,转换,生成都已经完成。
compiler 1 2 3 4 5 6 7 8 9 function compiler (input ) { const tokens = tokenizer(input); const ast = parser(tokens); const newAst = transformer(ast); const output = codeGenerator(newAst); return output; } const input = '(add 2 (subtract 4 2 "djwaqp"))' ;const output = compilter(input);
babel babel 的工作原理可以理解成就是一个简单的编译器:分析
=> 转换
=> 生成代码
bable 的篇幅太多,下面直接给出 demo源码 。 具体可以看 官方插件手册 babel-handbook
我们写的babel 插件,都是在转换的部分运行
自定义插件 babel-plugin-jsonz 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const t = require ('babel-types' );module .exports= function ( ) { return { visitor : { BinaryExpression (path, state ) { if (path.node.operator !== '===' ) { return ; } path.node.left = t.identifier('"sebmck"' ); path.node.right = t.identifier('"dork"' ); } } }; };
目标代码./src/test1.js 1 2 3 function demo ( ) { return 1 ===2 ; }
安装相关依赖:npm i babel-cli babel-types --save-dev
运行脚本: babel src/ -d build/
生成后的代码 ./build/test2.js 1 2 3 function demo ( ) { return "sebmck" === "dork" ; }
最后再推一波关于学习中看到的好网站
源码 esprima 解析语法树🌲 AST Explorer ast名词解释 babel-plugin-handbook