在看 babel 文档的时候,接触到 The Super Tiny Compiler,其中的注释感受解释的蛮容易理解,翻译记录一下。javascript
大部分的人在他们的平常工做中,实际上没有必要去思考编译器相关的东西,不关注编译器很正常。然而,编译器在你的身边很常见,你使用的不少工具,都是借鉴了编译器的概念。java
编译器的确很可怕。可是这是咱们(那些写编译器的人)本身的错误,咱们舍弃了简单合理,而且让它变得如此复杂可怕,以致于大部分的人认为是彻底没法接近的事情,只有书呆子能够明白。node
从编写一个最简单的编译器开始。这个编译器很是的小,若是你移除全部的注释,也只有 200 行代码。git
咱们准备写一个编译器,它的做用是将一些 LISP 方法调用的形式转换成 C 语言里面方法调用的的形式。github
若是你对其中的语言不太熟悉,我将会简单介绍一下。数组
若是咱们有两个方法 add
和 subtract
,它们会像下面这样书写:babel
example | LISP | C |
---|---|---|
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)) |
很容易,对吧?很好,这正是咱们准备要编译的。虽然这个不是完整的 LISP 或 C 语法,但它的语法足以演示大部分现代编译器的主要部分。工具
大部分的编译能够划分为 3 个主要阶段:解析(Parsing),转换(Transformation),代码生成(Code Generation)。ui
解析一般分为两个阶段:词法分析(Lexical Analysis)和语法分析(Syntactic Analysis)。翻译
例以下面的语法:
(add 2 (subtract 4 2))
记号看起来可能像这样:
[ { 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: ')' }, ]
抽象语法树看起来可能像这样:
{ 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', }] }] }] }
编译的下一个阶段就是转换。这一步只是获取上一步的 AST 而且再一次改变它。能够用相同的语言操做 AST,或者把 AST 转换成一个彻底新的语言。
让咱们看看若是转换一个 AST。
你可能发现咱们的 AST 里面有些元素很类似。这里有一些拥有 type
属性的对象,每个这样的对象被称为 AST 节点(AST Node)。这些节点定义了树上每一个单独部分的属性。
咱们有一个 NumberLiteral
节点:
{ type: 'NumberLiteral', value: '2', }
或者多是一个 CallExpression
节点:
{ type: 'CallExpression', name: 'subtract', params: [...nested nodes go here...], }
当转换 AST 时,咱们能够对节点的属性进行添加/移除/替换操做,咱们能够添加新的节点,移除节点,或者基于已存在的 AST 建立一个彻底新的 AST。
由于咱们的目标是一个新的语言,因此咱们将要针对新的语言,建立一个彻底新的 AST。
遍历(Traversal)
为了可以找到全部的节点,咱们须要遍历它们。这个遍历的过程要到达 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' }] }] }] }
上面的 AST,咱们将这样遍历:
若是直接操做这个 AST,这里可能要介绍各类抽象。可是咱们正在尝试作的事情,访问到树的每一个节点就足够了。
访问者(Visitors)
这里基本的思路是,建立一个“visitor”对象,它拥有的方法能够接受不一样类型的节点。
var visitor = { NumberLiteral() {}, CallExpression() {}, };
当咱们遍历 AST 时,只要“进入(enter)”到一个匹配的类型节点,咱们将调用这个 visitor 的方法。
为了让这个想法可行,咱们将传入一个节点和其父节点的引用。
var visitor = { NumberLiteral(node, parent) {}, CallExpression(node, parent) {}, };
而后,这里还存在“退出(exit)”的可能性。想象一下咱们这样的树结构:
- Program - CallExpression - NumberLiteral - CallExpression - NumberLiteral - NumberLiteral
当咱们遍历下去,最终会到达一个死胡同。因此当咱们完成树每一个分支的遍历,咱们就“退出(exit)”。所以,向下遍历树,“进入(enter)”到树节点,返回的时候,咱们就“退出(exit)”。
-> Program (enter) -> CallExpression (enter) -> Number Literal (enter) <- Number Literal (exit) -> Call Expression (enter) -> Number Literal (enter) <- Number Literal (exit) -> Number Literal (enter) <- Number Literal (exit) <- CallExpression (exit) <- CallExpression (exit) <- Program (exit)
为了支持这种功能,最后咱们的 visitor 看起来会像是这样:
var visitor = { NumberLiteral: { enter(node, parent) {}, exit(node, parent) {}, } };
编译的最后一个阶段就是代码生成。有些时候编译器在这个阶段,会作跟转换重叠的事情,可是大部分的代码生成只是意味着获取 AST 而且转换成字符串代码。
代码生成有几种不一样的运行方式,一些编译器会重用以前的记号(tokens),有些会建立一个单独的代码表示,这样他们就能够线性打印节点,可是从我了解到的状况,大部分会使用咱们刚才建立的 AST,也是咱们将要关注的。
咱们的代码生成器将有效地知道如何“打印(print)” AST 的全部不一样节点类型,而且它将递归地调用本身来打印嵌套的节点,直到将全部内容打印成一长串代码。
就这样!这些就是编译器全部不一样的部分。并非每个编译器都像我这里描述的那样。编译器用于不一样的目的,它们可能须要比我描述的更多的步骤。可是,如今你应该对于大部分编译器是什么样的,有一个更高的认识。
如今,我已经解释了这么多,你应该都能很好的写出本身的编译器,对吧?只是开个玩笑,这就是我要帮助的。那么就让咱们开始吧!