事物的特殊性往往也能反映事物的普遍性。-- 网摘
背景:在了解抽象语法树的时候偶然了解到 the-super-tiny-compiler这个开源项目,一看之下似乎不复杂,原文全是英文,阅读的时候就想到翻译一下,一方面加深理解,一方面也作为阅读的成果。
本文主要是对源码中注释的翻译。
大多数编译器分为三个主要阶段
一、解析
定义:将原始代码转成更抽象的表示。
详细解释:解析通常分为词法分析和句法分析两个阶段。
词法分析:由词法分析器将原始代码分解成不同的标记,这些标记包括数字、标签、标点符号、运算符等等,这取决于你怎么去定义词法分析器。
句法分析:将词法分析器产出的标记重新格式化为一种表示形式,该表示形式描述了语法的每个部分以及它们之间的关系。 这被称为中间表示或抽象语法树。抽象语法树,简称AST,是一个深层嵌套的对象,以易于使用的方式表示代码,并告诉我们很多信息。
举例:
类LISP语法:(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',
}]
}]
}]
}
二、转换
定义:利用代码更抽象的表示法添加编译器想做的任何操作。
详细解释:
1、变动:对拿到的AST做一些需要的改变,AST由很多语法节点组成,我们可以增加、删除、替换、更新树上的节点,当我们最终的目标语言是另一种语言时,也可以基于现有的语法树创建新的语法树。
2、遍历:采用深度优先遍历AST来获取节点。
比如有如下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'
}]
}]
}]
}
遍历顺序:
3、访问器:用于操作AST的节点
访问器模型:
/*
* -> 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)
*/
var visitor = {
NumberLiteral: {
enter(node, parent) {},
exit(node, parent) {},
}
};
三、生成代码
定义:将转换后的代码表示转化成新的源代码。
详细解释:实际上,代码生成器要知道怎么去输出AST中所有不同类型的节点,它要递归地调用自身去输出嵌套的节点,直到所有节点都被输出到一个长的存储代码的字符串中。
小结
以上就是一个编译器不同的部分。当然,也不是说所有的编译器就是上面描述的那样,基于不同的目的,编译器可能会有更多的步骤。
源码