编译原理
推荐在学习webpack前可以先学习the-super-tiny-compiler,主要是它的代码比较简短容易理解。webpack主要做的是将我们写的相关语法糖(JSX/es6等)转换成浏览器引擎能执行的代码,webpack中babel做事的原理同“the-super-tiny-compiler”,通过学习the-super-tiny-compiler,了解AST和编译原理。如果我们输入如下代码块
const input = '(add 2 (subtract 4 2))'
期望转换输出为
const output = 'add(2, subtract(4, 2));'
通过学习研究“the-super-tiny-compiler”, 你会了解上面的input通过那几步能输出output的结果
词法分析tokenizer
这里tokenizer主要是将输入的语法糖转换成tokens结构{type: 'type', value: 'value'}
function tokenizer(input) {
// current变量用于记录代码的位置,类似光标的概念
let current = 0;
// tokens是记录输入input转化后的结果
let tokens = [];
// 开始对字符串input进行遍历
while (current < input.length) {
// char记录当前遍历到current位置的字符
let char = input[current];
// 检查是否是左括号‘(’
if (char === '(') {
// 左括号的类型用'paren'标示
tokens.push({
type: 'paren',
value: '(',
});
// 指针+1
current++;
continue;
}
// 这里处理右括号‘(’,处理方式同上面左括号
if (char === ')') {
tokens.push({
type: 'paren',
value: ')',
});
current++;
continue;
}
// 处理空格,对于空格不需要存入到token中,移动current位置即可
let WHITESPACE = /\s/;
if (WHITESPACE.test(char)) {
current++;
continue;
}
// 处理数据
// (add 123 456)
// ^^^ ^^^
let NUMBERS = /[0-9]/;
if (NUMBERS.test(char)) {
// 记录数字
let value = '';
// 通过current后移,判断下一个位置上的char是否是数字,是就用value拼接记录
while (NUMBERS.test(char)) {
value += char;
char = input[++current];
}
// 将连续数字存入tokens中,type是number类型
tokens.push({ type: 'number', value });
continue;
}
// 处理字符串
// (concat "foo" "bar")
// ^^^ ^^^ string tokens
// 处理方式同上面number大致相同,不同的地方是通过双引号来判断开始和结束
if (char === '"') {
let value = '';
char = input[++current];
while (char !== '"') {
value += char;
char = input[++current];
}
char = input[++current];
tokens.push({ type: 'string', value });
continue;
}
// 处理方法名,处理方式同上面的字符串,区别是上面字符串可理解为变量,需要双引号包裹,这里的字符串是函数名,没有引号包裹
// Name token
// (add 2 4)
// ^^^
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;
}
执行tokens=tokenizer('(add 2 (subtract 4 2))')后获取到的tokens结果如下所示
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: ')' }
];
语法分析parser
parser它的主要功能是将tokens转换成ast结构
function parser(tokens) {
// current依然是记录当前指针的位置
let current = 0;
// 这里的walk主要是后面需要进行递归处理
function walk() {
let token = tokens[current];
// 处理type是number的token
if (token.type === 'number') {
current++;
// ast中number类型的type用NumberLiteral记录
return {
type: 'NumberLiteral',
value: token.value,
};
}
//处理type是string的token
if (token.type === 'string') {
current++;
// ast中string类型的type用StringLiteral记录
return {
type: 'StringLiteral',
value: token.value,
};
}
// 处理表达式,type: 'paren', 以左括号开始,并继续查找知道以右括号结束 ast中是以CallExpressions记录type,其中这里的node节点多了params参数
if (
token.type === 'paren' &&
token.value === '('
) {
token = tokens[++current];
let node = {
type: 'CallExpression',
name: token.value,
params: [],
};
token = tokens[++current];
// 查找type是paren,且以右括号结束的token,如果不是一直循环,
while (
(token.type !== 'paren') ||
(token.type === 'paren' && token.value !== ')')
) {
//对非结束token进行递归处理
node.params.push(walk());
token = tokens[current];
}
current++;
return node;
}
// 主要处理了number/string/paren类型的type, 对于未识别的type异常处理
throw new TypeError(token.type);
}
// 创建ast的根节点,type是Program
let ast = {
type: 'Program',
body: [],
};
// ast.body内容就是将token递归执行walk的过程
while (current < tokens.length) {
ast.body.push(walk());
}
return ast;
}
这里执行ast = parser(token)后获取的就是ast的数据,其结果如下所示
const 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'
}]
}]
}]
};
traverser
这里的遍历主要是在transformer中调用,先提前了解它的原理。接受2个参数ast和visitor,其中参数ast其结构如上面所示,visitor大概结构如下所示
visitor = {
NumberLiteral: {enter(node, parent) {}},
StringLiteral: {enter(node, parent) {}},
CallExpression: {enter(node, parent) {}},
}
traverser方法主要是对旧ast进行遍历,根据相应的type执行visitor传过来的enter方法
function traverser(ast, visitor) {
// 对array数组依次执行traverseNode方法
function traverseArray(array, parent) {
array.forEach(child => {
traverseNode(child, parent);
});
}
function traverseNode(node, parent) {
// methods主要是获取visitor对应type的方法
let methods = visitor[node.type];
// 执行methods方法
if (methods && methods.enter) {
methods.enter(node, parent);
}
// traverseArray依次处理各类型的子节点
switch (node.type) {
// 根节点
case 'Program':
traverseArray(node.body, node);
break;
// 表达式节点
case 'CallExpression':
traverseArray(node.params, node);
break;
// Number/String没有子节点,不用处理
case 'NumberLiteral':
case 'StringLiteral':
break;
default:
throw new TypeError(node.type);
}
if (methods && methods.exit) {
methods.exit(node, parent);
}
}
traverseNode(ast, null);
}
转换transformer
transformer主要是将ast中的数据进行处理获取到新的ast
function transformer(ast) {
// 创建新ast的根节点
let newAst = {
type: 'Program',
body: [],
};
// 将新的ast挂在旧ast的_context上(此方式修改了旧ast结构,存在数据污染行为),这里有点绕的点是下面的操作node._context操作,看着像是操作旧的ast,其实这里_context已经指向新的ast,所以newAst的代码会改变
ast._context = newAst.body;
// 执行上面的traverser方法
traverser(ast, {
NumberLiteral: {
enter(node, parent) {
//将number类型的子节点插入parent的_context属性上
parent._context.push({
type: 'NumberLiteral',
value: node.value,
});
},
},
StringLiteral: {
enter(node, parent) {
//将string类型的子节点插入parent的_context属性上
parent._context.push({
type: 'StringLiteral',
value: node.value,
});
},
},
CallExpression: {
enter(node, parent) {
// 这里添加了callee来记录name/type,
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name,
},
arguments: [],
};
node._context = expression.arguments;
if (parent.type !== 'CallExpression') {
// 这里新增了一个type, 它表示的是表达式语句
expression = {
type: 'ExpressionStatement',
expression: expression,
};
}
parent._context.push(expression);
},
}
});
return newAst;
}
其中newAst = transformer(ast),其结果如下所示,CallExpression的结构有变化,同时多了ExpressionStatement/Identifier类型
const newAst = {
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'
}]
}]
}
}]
};
生成代码
最后是遍历新的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);
}
}
最后这里的输出结果就是output = add(2, subtract(4, 2)),同时,在代码中能看到的compiler方法
function compiler(input) {
let tokens = tokenizer(input);
let ast = parser(tokens);
let newAst = transformer(ast);
let output = codeGenerator(newAst);
// and simply return the output!
return output;
}
其实,它主要就是分如下四部去完成编译的
- 根据词法分析将输入字符串变成tokens
- 根据语法分析将tokens转换成简单的ast结构
- 根据期望的visitor来修改ast结构,生成新的newAst,这里生成的结构和ast网站的结构更相符
- 最后将newAst转换成
参考链接
- the-super-tiny-compiler(github.com/jamiebuilds…)
- ast(astexplorer.net/#)
- 编译原理(www.ayqy.net/blog/the-su…