尽管大多数开发人员在日常工作中不需要开发编译器,但编译器的概念贯穿我们使用的许多工具和技术。我们也是可以抽空了解一下其原理。在 GitHub 上发现一篇很不错的编译器的代码库,就想着和大家一起分享学习一下。
介绍
这份代码库用易于阅读的 JavaScript 编写的现代编译器的所有主要部分的超简化示例
。
通读指导代码将帮助我们了解大多数编译器从头到尾的工作方式
。
其实就简单的几个函数方法,用不了多长时间就能看完!它将一个简单的 Lisp 方言
编译成对应的 AST(抽象语法树)并返回。
那就废话不多说,直接进入正题吧!
预习
前端开发中常见的编译器包括但不限于以下几种:
- Babel:用于将 ES6+ 代码转换为向后兼容的 JavaScript 代码,以便在当前和旧版本的浏览器中运行。
- TypeScript Compiler(tsc) :用于将 TypeScript 代码编译为 JavaScript 代码,使其可以在浏览器中执行。
- Sass / SCSS Compiler:用于将 Sass 或 SCSS 文件编译为普通的 CSS 文件,以便在浏览器中使用。
- Less Compiler:类似于 Sass 编译器,将 Less 文件编译为 CSS 文件。
- PostCSS:一个用 JavaScript 编写的工具,用于转换 CSS,并支持许多插件,可以进行自定义的 CSS 处理,例如添加浏览器前缀、压缩、提取变量等。
- Vue Loader 和 React JSX Compiler:这些工具用于将 Vue 和 React 组件编译为浏览器可执行的 JavaScript 代码。
而这些大多数编译器通常经历三个主要阶段:解析、转换、代码生成。
- 解析:将原始代码解析为更抽象的代码表示形式。
- 转换:使用抽象表示形式进行操作,执行编译器期望的转换。
- 代码生成:采用转换后的代码表示形式,并将其转换为新的代码形式。
首先,解析 通常包括词法分析和句法分析两个阶段:
- 词法分析:通过词法分析器(或称为 lexer),将原始代码拆分为一系列令牌(tokens)。这些令牌可以是数字、标识符、标点符号、运算符等。
- 句法分析:通过句法分析器,将这些令牌重新组织成描述代码语法结构的中间表示,也称为抽象语法树(Abstract Syntax Tree,AST)。AST 是一个嵌套的对象结构,用于表示代码的语法信息,以便进一步的处理和分析。
然后,转换阶段涉及对抽象语法树(AST)的修改。
- 在转换阶段,可以操作同一语言的 AST,也可以将其转换为全新的语言。
- 在 AST 中,有一些非常相似的元素,它们都具有类型属性。每个这样的对象被称为 AST 节点,它们定义了树中各个节点的属性,描述了树的各个部分。
- 需要按照深度优先的方式遍历 AST 中的每个节点。以达到遍历所有节点。
- 在转换 AST 时,我们可以进行以下操作:
- 添加、删除或替换节点的属性;
- 添加新节点或删除节点;
- 放弃现有的 AST,创建一个全新的 AST,以满足特定于目标语言的需求。
最后,大部分代码生成只是将我们的 AST 转换回字符串形式的代码。
- 代码生成器将知道如何 "打印" 所有不同类型的 AST 节点,并且它将递归地调用自身来打印嵌套节点,直到所有内容都打印成一个长的字符串。
源码学习
Let's begin...
tokenizer 词法分析器
js
function tokenizer(input) {
let current = 0;
let tokens = [];
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;
}
// 空格的情况
let WHITESPACE = /\s/;
if (WHITESPACE.test(char)) {
current++;
continue;
}
// 数字的情况
let NUMBERS = /[0-9]/;
if (NUMBERS.test(char)) {
let value = '';
while (NUMBERS.test(char)) {
value += char;
char = input[++current];
}
tokens.push({ type: 'number', value });
continue;
}
// 字符串的情况
if (char === '"') {
let value = '';
// 跳过开始的双引号
char = input[++current];
while (char !== '"') {
value += char;
char = input[++current];
}
// 跳过结束的双引号
char = input[++current];
tokens.push({ type: 'string', value });
continue;
}
// 函数的名字
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;
}
parser 解析器
js
// 把令牌数组转换成 AST
function parser(tokens) {
let current = 0;
// 创建一个递归函数
function walk() {
let token = tokens[current];
// 把每种类型的令牌分成不同的代码路径
// 数字
if (token.type === 'number') {
current++;
// 返回新的 AST 节点
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);
}
// 创建 AST
let ast = {
type: 'Program',
body: [],
};
while (current < tokens.length) {
ast.body.push(walk());
}
return ast;
}
traverser 遍历器
js
function traverser(ast, visitor) {
// 迭代数组,并调用 traverseNode 函数
function traverseArray(array, parent) {
array.forEach(child => {
traverseNode(child, parent);
});
}
// 接受 节点 参数和 父节点,都传给 visitor 方法
function traverseNode(node, parent) {
let methods = visitor[node.type];
if (methods && methods.enter) {
methods.enter(node, parent);
}
switch (node.type) {
// 先从顶层 `Program` 开始
case 'Program':
traverseArray(node.body, node);
break;
case 'CallExpression':
traverseArray(node.params, node);
break;
// 在 `NumberLiteral` 和 `StringLiteral` 的情况下,没有任何子节点可访问,所以将中断。
case 'NumberLiteral':
case 'StringLiteral':
break;
default:
throw new TypeError(node.type);
}
if (methods && methods.exit) {
methods.exit(node, parent);
}
}
// 启动遍历器
traverseNode(ast, null);
}
transformer 转换器
js
// 转换新的 AST
function transformer(ast) {
let newAst = {
type: 'Program',
body: [],
};
// 将在父节点上使用一个名为 `context` 的属性,将节点推送到其父节点的 `context` 中
// 这算是一种小技巧
ast._context = newAst.body;
// 用 ast 和 一个 visitor 方法触发遍历器
traverser(ast, {
// 第一个 visitor 方法
NumberLiteral: {
// enter 方法
enter(node, parent) {
// 创建一个名为"NumberLiteral"的新节点,并将其推送到父上下文。
parent._context.push({
type: 'NumberLiteral',
value: node.value,
});
},
},
// 下一个 `StringLiteral`
StringLiteral: {
enter(node, parent) {
parent._context.push({
type: 'StringLiteral',
value: node.value,
});
},
},
// 下一个 `CallExpression`.
CallExpression: {
enter(node, parent) {
// 创建一个带有嵌套 "Identifier" 的新节点 "CallExpression"。
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name,
},
arguments: [],
};
// 在原始的 "CallExpression" 节点上定义一个`_context`,
// 将引用 "expression" 的参数,以便可以推送参数。
node._context = expression.arguments;
// 父级节点不是 CallExpression
if (parent.type !== 'CallExpression') {
// 将用一个 `ExpressionStatement` 来包裹我们的 `CallExpression` 节点。
// 这样做是因为 JavaScript 中顶层的 `CallExpression` 实际上是语句
expression = {
type: 'ExpressionStatement',
expression: expression,
};
}
// 将被包装过的 `CallExpression` 推送到 `parent` 的 `context` 中
parent._context.push(expression);
},
}
});
// 返回新的 AST
return newAst;
}
codeGenerator 代码生成器
js
// 代码生成器将递归调用自身,将树中的每个节点打印成一个巨大的字符串。
function codeGenerator(node) {
// 按照节点的`type`来拆分处理
switch (node.type) {
// 如果有一个`Program`节点,将遍历`body`中的每个节点,
// 并将它们通过代码生成器运行,然后用换行符连接它们。
case 'Program':
return node.body.map(codeGenerator)
.join('\n');
// 对于 `ExpressionStatement`,将调用嵌套表达式的代码生成器,并添加一个分号...
case 'ExpressionStatement':
return (
codeGenerator(node.expression) +
';' // << (...because we like to code the *correct* way)
);
// 对于 `CallExpression`,将打印 `callee`,添加一个左括号,
// 然后将对 `arguments` 数组中的每个节点运行代码生成器,用逗号连接它们,
// 最后添加一个右括号。
case 'CallExpression':
return (
codeGenerator(node.callee) +
'(' +
node.arguments.map(codeGenerator)
.join(', ') +
')'
);
// 对于 `Identifier`,返回 `node` 的名称。
case 'Identifier':
return node.name;
// 对于 `NumberLiteral`,返回 `node` 的值。
case 'NumberLiteral':
return node.value;
// 对于 `StringLiteral`,将在 `node` 的值周围添加引号。
case 'StringLiteral':
return '"' + node.value + '"';
// 如果没有识别节点,将抛出一个错误。
default:
throw new TypeError(node.type);
}
}
compiler 编译器
最后!将创建 compiler
函数。在这里,将连接管道的每个部分。
- 输入 => 词法分析器 => 令牌
- 令牌 => 解析器 => 抽象语法树
- 抽象语法树 => 转换器 => 新的抽象语法树
- 新的抽象语法树 => 生成器 => 输出
js
function compiler(input) {
let tokens = tokenizer(input);
let ast = parser(tokens);
let newAst = transformer(ast);
let output = codeGenerator(newAst);
return output;
}
YOU MADE IT!
小结
这个编译器实现了一个非常简单的编译流程,可以将输入的代码字符串转换成目标代码字符串。虽然它非常简单,但展示了编译器的基本原理和流程。
我们是不是可以在此原理的基础上大刀阔斧的去创造了?!大家加油!
参考资料
github 源码:github.com/jamiebuilds...