本日我们将进修开辟一个编译器,然则呢,这个编译器并非说什么都能都编译,它只是一个超等小的编译器,重要用于申明编译器的一些基本的道理。
我们这个编译器可以将相似于lisp言语的函数挪用编译成相似于C言语的函数挪用。假如你对lisp言语和C言语这两者都不熟习,没紧要,什么言语实在无所谓,但接下来照样会给你一个疾速的引见。
假如我们有两个函数离别是add和subtract,假如用它们来盘算下面的表达式:
2 + 2
4 - 2
2 + (4 - 2)
那末在lisp言语中它可以长这模样:
(add 2 2) // 2 + 2
(subtract 4 2) // 4 - 2
(add 2 (subtract 4 2)) // 2 + (4 - 2)
而在C言语中它长这个模样:
add(2, 2)
subtract(4, 2)
add(2, subtract(4, 2))
相称简朴吧?
好吧,这是因为这仅仅只是我们这个编译器所须要处置惩罚的情况。 这既不是list言语的完整语法,也不是C言语的完整语法。 但这点语法已足以用来演示当代编译器所做的大部份事变。
大部份编译器所做的事变都可以剖析为三个重要的步鄹: 剖析、转换和代码天生。
剖析一般分为两个阶段:词法剖析和句法剖析。
关于下面的语法:
(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: ')' },
]
然后它对应的笼统语法树(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)。
你可以已注意到,我们的笼统语法树内里有一些异常相似的元素。 这些元素对象有一个type属性。 这每一个对象元素都被称为一个AST节点。 这些节点上定义的属性用于形貌AST树上的一个自力部份。
我们可以为数字字面量(NumberLiteral)竖立一个节点:
{
type: 'NumberLiteral',
value: '2',
}
或许是为挪用表达式(CallExpression)建立一个节点:
{
type: 'CallExpression',
name: 'subtract',
params: [...nested nodes go here...],
}
当转换AST树的时刻,我们可以须要对它举行add、remove、replace等操纵。 我们可以增添新节点,删除节点或许我们完整可以将AST树搁一边不睬,然后基于它建立一个全新的AST。
因为我们这个编译器的目的是将lisp言语转换成C言语,所以我们会聚焦建立一个特地用于目的言语(在这里是C言语)的全新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'
}]
}]
}]
}
所以关于上面的AST,我们须要像如许走:
假如我们直接操纵这个AST而不是建立一个零丁的AST,我们可以须要在这里引入种种笼统观点。 然则我们正在尝试做的事变,只须要接见树中的每一个节点就足够了。
运用“接见”这个词的缘由是因为这个词可以很好的表达怎样在对象构造上操纵元素。
这里最基本的思绪就是我们建立一个接见者对象,这个对象具有一些要领,这些要领可以吸收差别的节点范例。
比方下面如许:
var visitor = {
NumberLiteral() {},
CallExpression() {},
};
当我们遍历AST的时刻,一旦我们碰到一个与指定范例相婚配的节点,我们就会挪用接见者对象上的要领。
为了让这个函数比较好用,我们给它通报了该节点以及它的父节点:
var visitor = {
NumberLiteral(node, parent) {},
CallExpression(node, parent) {},
};
但是,这里也会有可以出如今退出时挪用东西。 设想一下我们前面提到的树构造:
- Program
- CallExpression
- NumberLiteral
- CallExpression
- NumberLiteral
- NumberLiteral
当我们往下遍历的时刻,我们会碰到终究的分支。 当我们接见完一切的分支后我们退出。 所以向下遍历树,我们进入节点,然后向上回溯的时刻我们退出节点。
-> 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树stringify一下输出,也就是转换成字符串输出。
代码天生有多种事变体式格局,有一些编译器会反复应用前面天生的标记,另一些编译器会建立代码的零丁示意,以便线性地打印节点,然则据我说知,大多数编译器的战略是运用我们方才建立的谁人AST,这是我们将要关注的。
实际上,我们的代码天生器将晓得怎样打印AST的一切差别节点范例,而且它将递归地挪用自身来打印嵌套节点,直到将一切内容打印成一长串代码。
而就是如许! 这就是编译器的一切差别部份。
如今不是说每一个编译器看起来都和我在这里形貌的完整一样。 编译器有很多差别的用处,他们可以须要比我细致的更多的步骤。
然则如今您应当对大多数编译器的表面有一个整体的高层次的观点。
如今我已诠释了一切这些,你应当可以写好自身的编译器了是吧?
只是在开顽笑的啦,我会在这里继承供应协助,所以我们最先吧!
前面说了,全部编译器或许可以分为三步:剖析、转换、代码天生。而剖析又可以分红两步:词法剖析和句法剖析。所以一共须要四个函数就可以完成了。我们来离别看一下:
我们将从编译器的第一步——剖析——最先,应用tokenizer函数举行词法剖析。
我们将把字符串代码拆分红由标记构成的数组:
(add 2 (subtract 4 2)) => [{ type: 'paren', value: '(' }, ...]
我们的tokenizer吸收一个代码字符串, 然后接下来做两个事变:
function tokenizer(input) {
// 一个current变量,相似于游标,用于跟踪我们在代码字符串中的位置
let current = 0;
// 以及一个tockens数组,用于将我们剖析的标记寄存个中
let tokens = [];
// 我们建立一个while轮回,在这内里我们设置我们的current变量,这个变量会跟着轮回的深切而不停增添
//
// 这么做是因为tockens可以会是恣意长度
while (current
let char = input[current];
// 我们起首要搜检的是左括弧,这个将会在稍后用于CallExpression,然则此时我们只体贴左括弧字符
//
// 我们搜检看看有无左括弧:
if (char === '(') {
// 假如有,则竖立一个对象,其type属性是paren,值为左括弧, 然后我们将这个对象到场tokens数组
tokens.push({
type: 'paren',
value: '(',
});
// 接着我们增添current变量,也就是挪动游标
current++;
// 然后举行下一轮轮回.
continue;
}
// 接着我们搜检右括弧,我们依据前面的套路来做:搜检右括弧,新增一个标记,增添current, 举行下一轮轮回
if (char === ')') {
tokens.push({
type: 'paren',
value: ')',
});
current++;
continue;
}
// 接着,我们搜检空缺格。 这很风趣,因为我们关注空缺格是因为它将字符串分离隔,然则我们并不须要将空缺格存为标记,我们
// 可以直接抛弃它,所以这里我们仅仅搜检空缺格是不是存在,假如它存在我们就进入下一轮轮回
let WHITESPACE = /\s/;
if (WHITESPACE.test(char)) {
current++;
continue;
}
// 下一个范例的标记是数字,这和我们前面见到的差别,因为一个数字多是恣意个字符构成,而且我们须要捕捉全部字符序列作为一个标记
//
// (add 123 456)
// ^^^ ^^^
// 比方上面的就只有两个自力的数字标记
//
// 所以当我们碰到序列中的第一个数字的时刻最先进一步处置惩罚.
let NUMBERS = /[0-9]/;
if (NUMBERS.test(char)) {
// 我们在这内里建立了一个value字符,用于拼接数字字符
let value = '';
// 接下来我们遍历背面的每一个字符直到碰到一个非数字字符,将这些字符和前面的value变量拼接起来, 而且转变current游标
while (NUMBERS.test(char)) {
value += char;
char = input[++current];
}
// 这以后我们将建立数字标记并到场tokens数组
tokens.push({ type: 'number', value });
// 然后我们继承
continue;
}
// 我们也支撑字符串,字符串就是用双引号(")包裹的一段文本,比方
//
// (concat "foo" "bar")
// ^^^ ^^^ 字符串标记
//
// 我们先搜检左双引号:
if (char === '"') {
// 建立一个value变量用于保留字符串.
let value = '';
// 我们将疏忽双引号,因为我们体贴的是双引号包裹的文本.
char = input[++current];
// 然后我们遍历背面的字符串,直到我们碰到右双引号
while (char !== '"') {
value += char;
char = input[++current];
}
// 疏忽右双引号,同理,因为我们体贴的是双引号包裹的文本.
char = input[++current];
// 建立范例为string的标记,并放进tockens数组
tokens.push({ type: 'string', value });
continue;
}
// 末了一种范例的标记是name标记,这是一串字符而不是数字,也就是lisp语法中的函数名
//
// (add 2 4)
// ^^^
// name 标记
//
let LETTERS = /[a-z]/i;
if (LETTERS.test(char)) {
let value = '';
// 同理,我们遍历,并将它们拼接起来
while (LETTERS.test(char)) {
value += char;
char = input[++current];
}
// 而且建立一个范例为name的标记,存储于tokens数组
tokens.push({ type: 'name', value });
continue;
}
// 末了,假如我们到这里还没有婚配一个字符, 我们将抛出一个毛病然后退出
throw new TypeError('I dont know what this character is: ' + char);
}
// 在tokenizer函数的末端我们将tokens数组返回
return tokens;
}
举个例子,关于(add 123 456)
这段lisp言语代码,tokenizer化以后获得的效果以下:
句法剖析的目的就是将tokens数组转换成AST。也就是下面的历程:
[{ type: 'paren', value: '(' }, ...] => { type: 'Program', body: [...] }
所以,我们定义一个parse函数,吸收我们的tokens数组作为参数:
function parser(tokens) {
// 一样我们保持一个current变量用作游标
let current = 0;
// 然则此次我们运用递归而不是while轮回,所以我们定义了walk函数
function walk() {
// 在walk函数内部,我们起首拿到tokens数组中current索引处寄存的标记
let token = tokens[current];
// 我们将把每种范例的标记以别的一种构造关联存储,以表现句法关联
// 起首从数字token最先
//
// 我们搜检看有无数字token
if (token.type === 'number') {
// 假如有,我们挪动游标
current++;
// 而且我们会返回一个叫做“NumberLiteral”的新的AST节点而且将它的value属性设置为我们标记对象的value属性
return {
type: 'NumberLiteral',
value: token.value,
};
}
// 假如我们有string范例的标记,我们会和数字范例相似,建立一个叫做“StringLiteral”的AST节点
if (token.type === 'string') {
//一样挪动游标
current++;
return {
type: 'StringLiteral',
value: token.value,
};
}
// 接下来我们查找CallExpressions. 我们是经由过程左括弧来最先这个历程的
if (
token.type === 'paren' &&
token.value === '('
) {
// 我们将疏忽左括弧,因为在AST内里,AST就是有句法关联的,所以我们不体贴左括弧自身了
token = tokens[++current];
// 我们建立一个叫做CallExpression的基本节点,而且将节点的名字设置为当前标记的value属性,
// 因为左括弧标记的下一个标记就是函数名字
let node = {
type: 'CallExpression',
name: token.value,
params: [],
};
// 我们挪动游标,疏忽掉name标记,因为函数名已存起在CallExpression中了
token = tokens[++current];
// 然后如今我们遍历每一个标记,找到CallExpression的参数直至碰到右括弧
//
// 如今,这里就是递归进场的处所了,为了防止堕入无穷的嵌套节点剖析,我们采纳递归的体式格局来搞定这个事变
//
// 为了更好的诠释这个东西,我们以我们的Lisp代码举例,你可以看到,add的参数是一个数字以及一个嵌套的CallExpression,
// 这个嵌套的函数挪用包含它自身的数字参数
//
// (add 2 (subtract 4 2))
//
// 你特可以从它的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: ')' }, <<<右括弧
// ]
//
// 我们将依赖于嵌套的walk函数来增添我们的游标
// 所以我们建立一个while轮回,这个while轮回将一向举行直到碰到一个范例是paren的标记而且这个标记的值是一个右括弧
while (
(token.type !== 'paren') ||
(token.type === 'paren' && token.value !== ')')
) {
// 我们将挪用walk函数,这个函数将返回一个节点, 我们将把这个返回的节点放到当前节点的params
// 数组中存储起来,如许嵌套关联再AST内里就表现出来了
node.params.push(walk());
token = tokens[current];
}
// 末了,我们须要末了一次挪动游标用于疏忽右括弧
current++;
// 而且返回节点
return node;
}
// 一样,假如我们没有辨认出标记的范例,我们也会抛出一个毛病
throw new TypeError(token.type);
}
// 如今walk函数已定义好了, 我们须要定义我们的AST树了,这个AST树有一个“Program”根节点:
let ast = {
type: 'Program',
body: [],
};
// 然后我们要启动我们的walk函数, 将AST节点放入根节点的body数组内里
//
// 我们在轮回内里做这个是因为,我们可以会碰到连着的多个函数挪用,比方说像如许的:
//
// (add 2 2)
// (subtract 4 2)
//启动walk
while (current
}
// 在剖析函数的末了,我们将返回天生的AST.
return ast;
}
任然之前面的例子举例,我们剖析后获得的AST以下:
如今我们已有了我们的AST,我们想要一个接见者可以接见差别的节点,不管什么时候婚配到对应的节点范例的时刻,我们都可以挪用接见者上的要领。
所以我们定义一个旅行者函数,这个函数吸收两个参数,第一个参数为AST树,第二个参数是一个接见者。这个接见者须要完成差别范例的AST节点须要挪用的一些要领:
traverse(ast, {
Program: {
enter(node, parent) {
// ...
},
exit(node, parent) {
// ...
},
},
CallExpression: {
enter(node, parent) {
// ...
},
exit(node, parent) {
// ...
},
},
NumberLiteral: {
enter(node, parent) {
// ...
},
exit(node, parent) {
// ...
},
},
});
因而,我们的旅行者函数的完成以下,它吸收AST和一个接见者作为参数,而且在内里还定义了两个要领:
function traverser(ast, visitor) {
// 定义一个traverseArray函数,可以是我们迭代一个数组,然后挪用我们稍后定义的traverseNode函数
function traverseArray(array, parent) {
array.forEach(child => {
traverseNode(child, parent);
});
}
// traverseNode函数吸收一个AST节点以及它的父节点,所以它也可以通报给我们的接见者函数
function traverseNode(node, parent) {
// 我们起首搜检接见者婚配范例的要领
let methods = visitor[node.type];
// 假如该AST节点范例存在enter要领,我们将以当前node及其父节点作为参数挪用该要领
if (methods && methods.enter) {
methods.enter(node, parent);
}
// 接下来我们会依据节点范例来把事变分别开来
switch (node.type) {
// 起首我们从顶级节点Program最先,因为该顶级节点有一个叫做body的属性,这个属性中是一个AST节点构成的数组
// 我们将挪用traverseArray函数来递归它
//
// (记着traverseArray函数会反过来挪用traverseNode函数,所以我们让这个AST被递归的接见)
case 'Program':
traverseArray(node.body, node);
break;
// 接下来我们对CallExpression节点做一样的事变,而且接见它们的参数
case 'CallExpression':
traverseArray(node.params, node);
break;
// 关于数字节点以及字符串节点,他们没有任何的子节点,所以我们直接break.
case 'NumberLiteral':
case 'StringLiteral':
break;
// 而且再一次,假如没有辨认出对应的节点范例,就抛出毛病
default:
throw new TypeError(node.type);
}
// 假如接见者上有exit要领,我们将以该节点和它的父节点作为参数挪用exit要领
if (methods && methods.exit) {
methods.exit(node, parent);
}
}
// 末了,我们启动traverser,这是经由过程挪用traverseNode完成的,而且traverseNode第二个参数是null,因为定级节点自身就没有父节点.
traverseNode(ast, null);
}
前面我们已写好了traverser函数,而traverser函数对节点的重要操纵都是经由过程它的第二个参数,也就是接见者来完成的,在上面,我们并没有定义接见者的详细完成,只是定义了enter和exit两个接口,实际上这两个接口所做的事变就是转换步鄹真正干的事变。为此我们定义transformer函数。
transformer函数吸收AST,将它通报给traverser函数,而且transformer函数内部还为traverser函数供应接见者。终究transformer函数返回一个新建的AST。
比方之前面谁人例子为例,获得的AST和转换后的AST以下:
----------------------------------------------------------------------------
Original AST | Transformed AST
----------------------------------------------------------------------------
{ | {
type: 'Program', | type: 'Program',
body: [{ | body: [{
type: 'CallExpression', | type: 'ExpressionStatement',
name: 'add', | expression: {
params: [{ | type: 'CallExpression',
type: 'NumberLiteral', | callee: {
value: '2' | type: 'Identifier',
}, { | name: 'add'
type: 'CallExpression', | },
name: 'subtract', | arguments: [{
params: [{ | type: 'NumberLiteral',
type: 'NumberLiteral', | value: '2'
value: '4' | }, {
}, { | type: 'CallExpression',
type: 'NumberLiteral', | callee: {
value: '2' | type: 'Identifier',
}] | name: 'subtract'
}] | },
}] | arguments: [{
} | type: 'NumberLiteral',
| value: '4'
-------------------------------- | }, {
| type: 'NumberLiteral',
| value: '2'
| }]
| }
| }
| }]
| }
----------------------------------------------------------------------------
所以我们的transformer函数的详细完成以下:
function transformer(ast) {
// 我们将建立一个新的AST(即newAst),它和我们本来的AST相似,有一个Program根节点
let newAst = {
type: 'Program',
body: [],
};
// 接下来,我们会做一些取巧的操纵,我们在父节点上定义一个\_context属性,
// 我们会将节点放入父节点的\_context属性中
// 一般你会有更好的笼统(或许会庞杂些),然则在这里我们如许做使得事变变得相对简朴
//
// 你仅仅须要记着的是,context是一个从老AST到新AST的援用
ast._cOntext= newAst.body;
// 我们以老ast和一个接见者作为参数挪用traverser函数
traverser(ast, {
// 第一个接见者的属性是用来处置惩罚NumberLiteral的
NumberLiteral: {
// 在enter要领中会对节点举行接见.
enter(node, parent) {
// 在这内里我们会建立一个新的AST节点,这个节点任然以NumberLiteral定名
// 我们会将这个节点放入该节点父亲的\_context属性中
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) {
// 我们建立一个新的节点CallExpression,它有一个嵌套的标识符
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name,
},
arguments: [],
};
// 接下来,我们在原始的CallExpression节点上定义一个新的context用于援用
// expression变量上的arguments属性
// 如许我们可以到场参数
node._cOntext= expression.arguments;
// 接着我们搜检父节点是不是是一个CallExpression节点
// 假如不是
if (parent.type !== 'CallExpression') {
// 我们将用一个ExpressionStatement节点包裹这个CallExpression节点
// 这么做是因为顶级CallExpression节点实际上就是statement
// 也就是说,假如某个CallExpression节点的父节点不是CallExpression节点
// 那末这个CallExpression节点应当就是函数声明
expression = {
type: 'ExpressionStatement',
expression: expression,
};
}
// 末了我们将这个新的CallExpression(可以被ExpressionStatement包裹)
// 放入parent._context
parent._context.push(expression);
},
}
});
// 在transformer函数的末了,我们把我们刚建立的新AST返回
return newAst;
}
我们一样之前面的例子来看一下新建立AST长什么模样:
如今让我们进入我们的末了一个步鄹:代码天生。我们的代码天生函数会递归的挪用自身用来打印它的节点到一个很大的字符串。也就是完成由newAST到代码的历程:
newAst => generator => output
function codeGenerator(node) {
// 我们会依据节点的type范例来将事变离别处置惩罚
switch (node.type) {
// 假如我们有一个Program节点,我们将遍历body中的每一个节点而且对每一个节点递挪用codeGenerator
// 函数,而且将它们的效果用一个换行符连接起来
case 'Program':
return node.body.map(codeGenerator)
.join('\n');
// 关于ExpressionStatement节点,我们将在节点的expression节点上挪用
// codeGenerator函数,然后我们会加上一个分号(即;)
case 'ExpressionStatement':
return (
codeGenerator(node.expression) +
';' // <<(...because we like to code the *correct* way)
);
// 关于CallExpression节点,我们会打印callee并最先一个做括弧
// 我们会遍历该节点的arguments属性,然后对每一个属性挪用codeGenerator要领,
// 将他们的效果用逗号分开,末了在背面加一个右括弧
case 'CallExpression':
return (
codeGenerator(node.callee) +
'(' +
node.arguments.map(codeGenerator)
.join(', ') +
')'
);
// 关于标识符,我们将返回节点的名字
case 'Identifier':
return node.name;
// 关于NumberLiteral节点,我们返回它的value属性
case 'NumberLiteral':
return node.value;
// 关于StringLiteral节点,我们用引号将它的value属性值包裹起来
case 'StringLiteral':
return '"' + node.value + '"';
// 假如没有辨认节点,我们将抛出毛病
default:
throw new TypeError(node.type);
}
}
一样以上面例子举例,它的输出效果如图:
如今,编译器的三大步鄹的代码都已完成了,我们如今最先完成编译器,它的体式格局就是将三个步鄹链接起来,可以将这几个步鄹形貌以下:
1. input => tokenizer => tokens
2. tokens => parser => ast
3. ast => transformer => newAst
4. newAst => generator => output
我们的编译器代码以下:
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;
}
末了作为一个模块,我们愿望别人去运用它,因为我们的每一个函数都是相对自力的一个功能模块,所以我们将这内里的每一个函数都导出:
module.exports = {
tokenizer,
parser,
traverser,
transformer,
codeGenerator,
compiler,
};
更多相干和无关内容迎接阅读Github和个人博客
书把手系列还包含:手把手教你完成一个简朴的Promise,手把手教你完成一个简朴的MVC形式,手把手教你完成一个简朴的MVP形式,手把手教你完成一个简朴的MVVM形式。