理解 AST 实现和编译原理
AST 技术是现代化前端基建和工程化建设的基石:Babel、Webpack、ESLint、代码压缩工具等耳熟能详的工程化基建工具或流程,都离不开 AST 技术;Vue、React 等经典前端框架,也离不开基于 AST 技术的编译。
# AST 基础知识
先对 AST 下一个定义,AST 是 Abstract Syntax Tree 的缩写,表示抽象语法树:
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax Tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似 if-condition-then 这样的条件跳转语句,可以使用带有三个分支的节点来表示。
AST 的应用场景经常出现在源代码的编译过程中:一般语法分析器创建出 AST,然后 AST 在语义分析阶段添加一些信息,甚至修改 AST 内容,最终产出编译后代码。
# AST 初体验
了解了 AST 基本概念,对 AST 进行一个“感官认知”。这里提供给你一个平台:AST explorer (opens new window),在这个平台中,可以实时看到 JavaScript 代码转换为 AST 之后的产出结果。如下图所示:
AST 在线分析结果图
可以看到,经过 AST 转换,的 JavaScript 代码(左侧)变成了一种 ESTree 规范的数据结构(右侧),这种数据结构就是 AST。
这个平台实际使用了 acorn (opens new window) 作为 AST 解析器。
# acorn 解析
实际上,社区上多项著名项目都依赖的 acorn 的能力(比如 ESLint、Babel、Vue.js 等),acorn 的介绍为:
A tiny, fast JavaScript parser, written completely in JavaScript.
由此可知,acorn 是一个完全使用 JavaScript 实现的、小型且快速的 JavaScript 解析器。基本用法非常简单,代码如下:
let acorn = require('acorn')
let code = 1 + 2
console.log(acorn.parse(code))
2
3
更多使用方式不再一一列举。你可以结合相关源码 (opens new window)进一步学习。
将视线更多地聚焦 acorn 的内部实现中。对所有语法解析器来说,实现流程上很简单,如下图所示:
acorn 工作流程图
源代码经过词法分析,即分词得到 Token 序列,对 Token 序列进行语法分析,得到最终 AST 结果。但 acorn 稍有不同的是:acorn 将词法分析和语法分析交替进行,只需要扫描一遍代码即可得到最终 AST 结果。
acorn 的 Parser 类源码 (opens new window)形如:
export class Parser {
constructor(options, input, startPos) {
//...
}
parse() {
// ...
}
// 判断所处 context
get inFunction() { return (this.currentVarScope().flags & SCOPE_FUNCTION) > 0 }
get inGenerator() { return (this.currentVarScope().flags & SCOPE_GENERATOR) > 0 }
get inAsync() { return (this.currentVarScope().flags & SCOPE_ASYNC) > 0 }
get allowSuper() { return (this.currentThisScope().flags & SCOPE_SUPER) > 0 }
get allowDirectSuper() { return (this.currentThisScope().flags & SCOPE_DIRECT_SUPER) > 0 }
get treatFunctionsAsVar() { return this.treatFunctionsAsVarInScope(this.currentScope()) }
get inNonArrowFunction() { return (this.currentThisScope().flags & SCOPE_FUNCTION) > 0 }
static extend(...plugins) {
// ...
}
// 解析入口
static parse(input, options) {
return new this(options, input).parse()
}
static parseExpressionAt(input, pos, options) {
let parser = new this(options, input, pos)
parser.nextToken()
return parser.parseExpression()
}
// 分词入口
static tokenizer(input, options) {
return new this(options, input)
}
}
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
稍做解释:
- type 表示当前 Token 类型;
- pos 表示当前 Token 所在源代码中的位置;
- startNode 方法返回当前 AST 节点;
- nextToken 方法从源代码中读取下一个 Token;
- parseTopLevel 方法实现递归向下组装 AST 树。
这是 acorn 实现解析 AST 的入口骨架,实际的分词环节主要解决以下问题。
- 明确需要分析哪些 Token 类型。
- 关键字:import,function,return 等
- 变量名称
- 运算符号
- 结束符号
- 状态机:简单来讲就是消费每一个源代码中的字符,对字符意义进行状态机判断。以“对于
/
的处理”为例,对于3/10
的源代码,/
就表示一个运算符号;对于var re = /ab+c/
源代码来说,/
就表示正则运算的起始字符了。
在分词过程中,实现者往往使用一个 Context 来表达一个上下文,实际上Context 是一个栈数据结果(这一部分源码你可以点击这里 (opens new window)阅读)。
acorn 在语法解析阶段主要完成 AST 的封装以及错误抛出。在这个过程中,需要你了解,一段源代码可以用:
- Program——整个程序
- Statement——语句
- Expression——表达式
来描述。
当然,Program 包含了多段 Statement,Statement 又由多个 Expression 或者 Statement 组成。这三种大元素,就构成了遵循 ESTree 规范的 AST。最终的 AST 产出,也是这三种元素的数据结构拼合。具体实现代码不再探究。
下面通过 acorn 以及一个脚本,来实现非常简易的 Tree Shaking 能力。
# AST 实战演练——实现一个简易 Tree Shaking 脚本
实现一个简单的 DCE(dead code elimination)。
目标如下,实现一个 Node.js 脚本 treeShaking.js,执行命令:
node treeShaking test.js
可以将test.js
中的 dead code 消除。使用test.js
测试代码如下:
function add(a, b) {
return a + b
}
function multiple(a, b) {
return a * b
}
var firstOp = 9
var secondOp = 10
add(firstOp, secondOp)
2
3
4
5
6
7
8
9
理论上讲,上述代码中的multiple
方法可以被“摇掉”。
进入实现环节,首先请看下图,了解整体架构流程:
基于 AST 的 tree-shaking 简易实现
设计 JSEmitter 类,用于根据 AST 产出 JavaScript 代码(js-emitter.js 文件内容):
class JSEmitter {
// 访问变量声明,以下都是工具方法
visitVariableDeclaration(node) {
let str = ''
str += node.kind + ' '
str += this.visitNodes(node.declarations)
return str + '\n'
}
visitVariableDeclarator(node, kind) {
let str = ''
str += kind ? kind + ' ' : str
str += this.visitNode(node.id)
str += '='
str += this.visitNode(node.init)
return str + ';' + '\n'
}
visitIdentifier(node) {
return node.name
}
visitLiteral(node) {
return node.raw
}
visitBinaryExpression(node) {
let str = ''
str += this.visitNode(node.left)
str += node.operator
str += this.visitNode(node.right)
return str + '\n'
}
visitFunctionDeclaration(node) {
let str = 'function '
str += this.visitNode(node.id)
str += '('
for (let param = 0; param < node.params.length; param++) {
str += this.visitNode(node.params[param])
str += ((node.params[param] == undefined) ? '' : ',')
}
str = str.slice(0, str.length - 1)
str += '){'
str += this.visitNode(node.body)
str += '}'
return str + '\n'
}
visitBlockStatement(node) {
let str = ''
str += this.visitNodes(node.body)
return str
}
visitCallExpression(node) {
let str = ''
const callee = this.visitIdentifier(node.callee)
str += callee + '('
for (const arg of node.arguments) {
str += this.visitNode(arg) + ','
}
str = str.slice(0, str.length - 1)
str += ');'
return str + '\n'
}
visitReturnStatement(node) {
let str = 'return ';
str += this.visitNode(node.argument)
return str + '\n'
}
visitExpressionStatement(node) {
return this.visitNode(node.expression)
}
visitNodes(nodes) {
let str = ''
for (const node of nodes) {
str += this.visitNode(node)
}
return str
}
// 根据类型,执行相关处理函数
visitNode(node) {
let str = ''
switch (node.type) {
case 'VariableDeclaration':
str += this.visitVariableDeclaration(node)
break;
case 'VariableDeclarator':
str += this.visitVariableDeclarator(node)
break;
case 'Literal':
str += this.visitLiteral(node)
break;
case 'Identifier':
str += this.visitIdentifier(node)
break;
case 'BinaryExpression':
str += this.visitBinaryExpression(node)
break;
case 'FunctionDeclaration':
str += this.visitFunctionDeclaration(node)
break;
case 'BlockStatement':
str += this.visitBlockStatement(node)
break;
case "CallExpression":
str += this.visitCallExpression(node)
break;
case "ReturnStatement":
str += this.visitReturnStatement(node)
break;
case "ExpressionStatement":
str += this.visitExpressionStatement(node)
break;
}
return str
}
// 入口
run(body) {
let str = ''
str += this.visitNodes(body)
return str
}
}
module.exports = JSEmitter
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
来具体分析一下,JSEmitter 类中创建了很多 visitXXX 方法,他们最终都会产出 JavaScript 代码。继续结合treeShaking.js
的实现来理解:
const acorn = require("acorn")
const l = console.log
const JSEmitter = require('./js-emitter')
const fs = require('fs')
// 获取命令行参数
const args = process.argv[2]
const buffer = fs.readFileSync(args).toString()
const body = acorn.parse(buffer).body
const jsEmitter = new JSEmitter()
let decls = new Map()
let calledDecls = []
let code = []
// 遍历处理
body.forEach(function(node) {
if (node.type == "FunctionDeclaration") {
const code = jsEmitter.run([node])
decls.set(jsEmitter.visitNode(node.id), code)
return;
}
if (node.type == "ExpressionStatement") {
if (node.expression.type == "CallExpression") {
const callNode = node.expression
calledDecls.push(jsEmitter.visitIdentifier(callNode.callee))
const args = callNode.arguments
for (const arg of args) {
if (arg.type == "Identifier") {
calledDecls.push(jsEmitter.visitNode(arg))
}
}
}
}
if (node.type == "VariableDeclaration") {
const kind = node.kind
for (const decl of node.declarations) {
decls.set(jsEmitter.visitNode(decl.id), jsEmitter.visitVariableDeclarator(decl, kind))
}
return
}
if (node.type == "Identifier") {
calledDecls.push(node.name)
}
code.push(jsEmitter.run([node]))
});
// 生成 code
code = calledDecls.map(c => {
return decls.get(c)
}).concat([code]).join('')
fs.writeFileSync('test.shaked.js', code)
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
对于上面代码分析,首先通过process.argv
获取到目标文件,对于目标文件通过fs.readFileSync()
方法读出字符串形式的内容buffer
,对于这个buffer
变量,使用acorn.parse
进行解析,并对产出内容进行遍历。
在遍历过程中,对于不同的节点类型,调用 JS Emitter 实例不同的处理方法。在整个过程中,维护了:
- decls——Map 类型
- calledDecls——数组类型
- code——数组类型
三个关键变量。decls
存储所有的函数或变量声明类型节点,calledDecls
则存储了代码中真正使用到的数或变量声明,code
存储了其他所有没有被节点类型匹配的 AST 部分。
下面来分析具体的遍历过程。
- 在遍历过程中,对所有函数和变量的声明,都维护到
decls
中。 - 接着,对所有的 CallExpression 和 IDentifier 进行检测。因为 CallExpression 代表了一次函数调用,因此在该 if 条件分支内,将相关函数节点调用情况推入到
calledDecls
数组中,同时对于该函数的参数变量也推入到calledDecls
数组。因为 IDentifier 代表了一个变量的取值,也推入到calledDecls
数组。
经过整个 AST 遍历,就可以只遍历calledDecls
数组,并从decls
变量中获取使用到的变量和函数声明,最终使用concat
方法合并带入code
变量中,使用join
方法转化为字符串类型。