热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

手把手教你完成一个简朴的编译器

手把手教你完成一个简朴的编译器1、概述本日我们将进修开辟一个编译器,然则呢,这个编译器并非说什么都能都编译,它只是一个超等小的编译器,重要用于申明编译器的一些基本的道理。我们这个编
手把手教你完成一个简朴的编译器

1、 概述

本日我们将进修开辟一个编译器,然则呢,这个编译器并非说什么都能都编译,它只是一个超等小的编译器,重要用于申明编译器的一些基本的道理。

《手把手教你完成一个简朴的编译器》

我们这个编译器可以将相似于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言语的完整语法。 但这点语法已足以用来演示当代编译器所做的大部份事变。

大部份编译器所做的事变都可以剖析为三个重要的步鄹: 剖析、转换和代码天生。

  1. 剖析。 剖析就是将原始代码转换成代码的笼统示意。
  2. 转换。 转换就是以这个笼统示意为基本,做编译器想做的任何事变。
  3. 代码天生。 代码天生就是将转换后的笼统示意变成新的代码。

2、 剖析

剖析一般分为两个阶段:词法剖析句法剖析

  1. 词法剖析。 词法剖析一般是运用一个标记器(或词法剖析器)将原始代码拆分红叫做标记的东西。而标记是一些细小的对象构成的数组,它们一般用来形貌一些伶仃的语法片断,它们可以是数字、标签、标点符号、操纵符等等。
  2. 语法剖析。 语法剖析将词法剖析获得的标记从新花样化为用于形貌语法的每一个部份及其相互关联的示意。 这被称为中心示意笼统语法树(AST)。笼统语法树(简称AST)是一个深度嵌套的对象,用于以一种既好用又能供应很多信息的情势表式代码。

关于下面的语法:

(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',
}]
}]
}]
}

3、 转换

在剖析以后,编译器的下一步鄹是转换。 一样,这不过就是将末了一步的笼统语法树(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。

3.1 遍历

为了阅读一切这些节点,我们须要可以遍历它们。 这个遍历历程是对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,我们须要像如许走:

  1. Program – 从AST树的顶层最先。
  2. CallExpression (add) – 挪动到Program的body属性的第一个元素。
  3. NumberLiteral (2) – 挪动到CallExpression(add)的第一个参数。
  4. CallExpression (subtract) – 挪动到CallExpression(add)的第二个参数。
  5. NumberLiteral (4) – 挪动到CallExpression(subtract)的第一个参数。
  6. NumberLiteral (2) – 挪动到CallExpression(subtract)的第二个参数。

假如我们直接操纵这个AST而不是建立一个零丁的AST,我们可以须要在这里引入种种笼统观点。 然则我们正在尝试做的事变,只须要接见树中的每一个节点就足够了。

运用“接见”这个词的缘由是因为这个词可以很好的表达怎样在对象构造上操纵元素。

3.2 接见者

这里最基本的思绪就是我们建立一个接见者对象,这个对象具有一些要领,这些要领可以吸收差别的节点范例。

比方下面如许:

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) {},
}
};

4、 代码天生

编译器的末了一步是代码天生。有时刻编译器在这一步会反复做一些转换步鄹做过的事变。 然则对代码天生而言,它所做的大部份事变就是将我们的AST树stringify一下输出,也就是转换成字符串输出。

代码天生有多种事变体式格局,有一些编译器会反复应用前面天生的标记,另一些编译器会建立代码的零丁示意,以便线性地打印节点,然则据我说知,大多数编译器的战略是运用我们方才建立的谁人AST,这是我们将要关注的。

实际上,我们的代码天生器将晓得怎样打印AST的一切差别节点范例,而且它将递归地挪用自身来打印嵌套节点,直到将一切内容打印成一长串代码。

而就是如许! 这就是编译器的一切差别部份。

如今不是说每一个编译器看起来都和我在这里形貌的完整一样。 编译器有很多差别的用处,他们可以须要比我细致的更多的步骤。

然则如今您应当对大多数编译器的表面有一个整体的高层次的观点。

如今我已诠释了一切这些,你应当可以写好自身的编译器了是吧?

只是在开顽笑的啦,我会在这里继承供应协助,所以我们最先吧!

5、编译器的代码完成

前面说了,全部编译器或许可以分为三步:剖析、转换、代码天生。而剖析又可以分红两步:词法剖析和句法剖析。所以一共须要四个函数就可以完成了。我们来离别看一下:

5.1、 剖析的完成

5.1.1、 词法剖析之tokenizer完成

我们将从编译器的第一步——剖析——最先,应用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 // 我们还会存储变量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化以后获得的效果以下:

《手把手教你完成一个简朴的编译器》

5.1.2、 句法剖析之parser完成

句法剖析的目的就是将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.body.push(walk());
}
// 在剖析函数的末了,我们将返回天生的AST.
return ast;
}

任然之前面的例子举例,我们剖析后获得的AST以下:

《手把手教你完成一个简朴的编译器》

5.2、 转换的完成

如今我们已有了我们的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) {
// ...
},
},
});

5.2.1 、traverser函数完成

因而,我们的旅行者函数的完成以下,它吸收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);
}

5.2.2 、transformer函数完成

前面我们已写好了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长什么模样:

《手把手教你完成一个简朴的编译器》

5.3、 代码天生的完成

如今让我们进入我们的末了一个步鄹:代码天生。我们的代码天生函数会递归的挪用自身用来打印它的节点到一个很大的字符串。也就是完成由newAST到代码的历程:

newAst => generator => output

5.3.1 codeGenerator的完成

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);
}
}

一样以上面例子举例,它的输出效果如图:

《手把手教你完成一个简朴的编译器》

6、编译器(compiler)的完成

如今,编译器的三大步鄹的代码都已完成了,我们如今最先完成编译器,它的体式格局就是将三个步鄹链接起来,可以将这几个步鄹形貌以下:

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形式。


推荐阅读
  • IjustinheritedsomewebpageswhichusesMooTools.IneverusedMooTools.NowIneedtoaddsomef ... [详细]
  • 标题: ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • vue使用
    关键词: ... [详细]
  • javascript  – 概述在Firefox上无法正常工作
    我试图提出一些自定义大纲,以达到一些Web可访问性建议.但我不能用Firefox制作.这就是它在Chrome上的外观:而那个图标实际上是一个锚点.在Firefox上,它只概述了整个 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • PHP图片截取方法及应用实例
    本文介绍了使用PHP动态切割JPEG图片的方法,并提供了应用实例,包括截取视频图、提取文章内容中的图片地址、裁切图片等问题。详细介绍了相关的PHP函数和参数的使用,以及图片切割的具体步骤。同时,还提供了一些注意事项和优化建议。通过本文的学习,读者可以掌握PHP图片截取的技巧,实现自己的需求。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
  • 006_Redis的List数据类型
    1.List类型是一个链表结构的集合,主要功能有push,pop,获取元素等。List类型是一个双端链表的结构,我们可以通过相关操作进行集合的头部或者尾部添加删除元素,List的设 ... [详细]
author-avatar
再见要死不活的_454
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有